Skip to content

fix(sts): enforce AssumeRole trust with the real caller - #1442

Draft
NitinKumar004 wants to merge 1 commit into
developmentfrom
fix/sts-assumerole-trust
Draft

NitinKumar004 wants to merge 1 commit into
developmentfrom
fix/sts-assumerole-trust

Conversation

@NitinKumar004

Copy link
Copy Markdown
Collaborator

AUTHZ-X1d. Also closes tracker row STS-X2.

Before this change, AssumeRole always evaluated the trust policy as if the account root were calling, whoever actually signed the request. Under --enforce-auth, any caller with sts:AssumeRole could therefore assume any role that trusted the account. A role that named one specific user could not be assumed by that user unless they also had an identity allow. Separately, the role was looked up by the last segment of the RoleArn, so arn:aws:iam::999999999999:role/A or role/x/A would assume the local role A.

What changed

IAM: EvaluateTrust (providers/aws/iam/trust_policy.go, new driver.TrustEvaluator)

  • Principal matching depends on the principal type. Only "AWS" principals, or the plain string "*", can match a SigV4 caller. Service, Federated and CanonicalUser never match one.
  • Inside "AWS":
    • "*" matches anyone.
    • arn:aws:iam::ACCT:root or a bare ACCT matches any caller in that account.
    • An exact caller ARN matches and sets NamedDirectly.
    • ARNs are not wildcard-matched.
  • NotPrincipal is honoured on Deny statements. The caller escapes the Deny only when every ARN in its chain (the role and the session, for a role session) and the account are listed.
  • Condition is evaluated through the existing condition engine. Role tags are visible as aws:ResourceTag/*. ForAllValues: and ForAnyValue: now work on multivalued keys, which aws:TagKeys needs.
  • EvaluateAssumeRoleTrust is unchanged, so the auth-off path behaves exactly as before.

STS: the decision under --enforce-auth (server/aws/sts/trust.go)

  • The AssumeRole checks are now ResourcePolicy checks. The gate records the identity decision, and the handler finishes it:
    allowed = roleExists && trust.Allow && !trust.ExplicitDeny && identity != explicitDeny && (identity == allowed || trust.NamedDirectly).
  • Which caller ARNs are matched:
    • IAM user: the user ARN.
    • Role session: the role ARN (with path) and the assumed-role ARN.
    • Federated user: its own ARN.
    • Root: the root ARN.
  • The condition context adds these keys:
    • sts:ExternalId, sts:RoleSessionName and sts:SourceIdentity;
    • aws:RequestTag/*, aws:TagKeys and sts:TransitiveTagKeys;
    • for a role session, aws:PrincipalArn set to the role ARN.
  • Passing tags also requires sts:TagSession (in both the trust policy and the identity policy, same rule as above). Passing SourceIdentity also requires sts:SetSourceIdentity.
  • Every refusal returns User: <caller> is not authorized to perform: sts:AssumeRole on resource: <RoleArn as sent>. The message is the same whether or not the role exists.
  • Signed AssumeRoleWithWebIdentity and AssumeRoleWithSAML now return 403 AccessDenied under --enforce-auth, because the token or assertion is not validated. Unsigned calls still get MissingAuthenticationToken. Auth-off behaviour is unchanged.

Session kinds (SessionOwner.Kind replaces Role bool)

The gate refuses the following calls whatever the policies say:

Credential Refused calls
GetFederationToken all IAM; all STS except GetCallerIdentity
GetSessionToken all IAM (MFA is not verified, so the MFA exception never applies); all STS except AssumeRole and GetCallerIdentity
Role session GetSessionToken and GetFederationToken

STS-X2

The RoleArn must equal the stored ARN of the role, including account and path. If it doesn't, the call is refused like a missing role: AccessDenied, naming the RoleArn as sent. This applies with auth on and off, whenever IAM is wired.

Sources (re-checked during research)

  • IAM User Guide, "Policy evaluation logic", and "How AWS enforcement code logic evaluates requests to allow or deny access":
    • Role trust policies must explicitly allow the principal.
    • Within one account, a resource policy that names the user, role session or federated user directly is enough on its own.
    • A trust that names the account still needs an identity allow.
  • IAM User Guide, "How IAM roles differ from resource-based policies" and "Roles terms and concepts: trust policy".
  • STS API reference, AssumeRole: Permissions, ExternalId, Tags (sts:TagSession) and SourceIdentity (sts:SetSourceIdentity).
  • IAM User Guide, "Compare AWS STS credentials": which APIs each kind of temporary credential can call.
  • IAM User Guide, "AWS JSON policy elements: NotPrincipal": a Deny must list the session, the role and the account to exclude a role session.

Known gaps (tracked separately)

  • Session policies are not intersected (AUTHZ-X1j).
  • The 1h cap on role chaining (STS-04).
  • GetFederationToken without a policy (STS-07).
  • Validated web identity (AUTHZ-X1i).
  • A trust naming a role ARN is treated as naming the session directly, so a permissions boundary's implicit deny does not limit it.

Verification

  • Tests were written first and failed on the old code:

    • server/aws/sts/trust_enforced_test.go (real SDK against an enforced server);
    • providers/aws/iam/evaluate_trust_test.go;
    • the STS trust rows in server/aws/authz_matrix_test.go;
    • TestEnforceAuthAssumeRoleTrust in contrib/server.
  • Commands that pass:

    • go build ./...
    • go vet and go test -race on providers/aws/iam, server/aws/... and server/wire/...
    • go -C contrib/server test -run Enforce
    • golangci-lint --new-from-rev=origin/development (root and contrib/server): 0 issues
    • coveragegen, which regenerated docs/coverage for the new TrustEvaluator
  • Real aws CLI against cloudemu serve --enforce-auth, bootstrapped through the admin token and seeded iamUsers:

    Check Result
    sts assume-role without --external-id AccessDenied
    sts assume-role with --external-id succeeds
    The resulting session can call DynamoDB and gets AccessDenied on IAM
    Role trusting only another user AccessDenied
    Role trusting the account root AccessDenied until the caller gets sts:AssumeRole, then succeeds
    Role chaining succeeds into a role that trusts the first role, and is refused for one that doesn't
    Federation token calling AssumeRole AccessDenied (GetCallerIdentity still works)
    Foreign-account RoleArn AccessDenied
    Wrong-path RoleArn AccessDenied

Under --enforce-auth the role trust policy is now evaluated for the signed caller, with principal types, NotPrincipal and conditions. Session credentials are limited to the STS and IAM calls AWS allows each kind, signed web identity and SAML calls are refused, and a RoleArn with another account or path no longer assumes the local role.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant