Assuming an AWS IAM Role via Federation (STS)
1. Why: the problem this solves
Before federation, giving an external identity --- a human at a partner company, a CI/CD pipeline, a mobile app user --- access to AWS meant one of two bad options:
- Create an IAM user for them. Now you're managing a long-lived
AccessKeyId/SecretAccessKeypair outside AWS's control: it doesn't expire, it can leak into a repo or a laptop, and revoking it requires you to notice and act. - Hand out your own credentials. Worse --- no per-person audit trail, no way to scope what they can touch, no way to revoke one user without breaking everyone.
Neither option scales past a handful of users, and both fail the same test: the credential outlives the need for it. A CI job that runs for 90 seconds shouldn't hold AWS keys that are valid forever. A contractor who leaves the company shouldn't require someone to remember to delete an IAM user.
What's actually needed is a way to say: "Trust proof of identity from a system I already trust (our IdP, GitHub, Google), and in exchange, hand out AWS credentials that work for a few minutes and then evaporate." That's exactly what AWS Security Token Service (STS) federation does --- it trades an authentication token from an external Identity Provider (IdP) for temporary, scoped, self-expiring AWS credentials.
2. What: definition and boundaries
AWS STS federation is a token-exchange service: it accepts a signed proof of identity issued by an external IdP and, if that IdP and the target role both agree to trust each other, returns short-lived AWS credentials scoped to one IAM role.
What it is not:
- It does not authenticate anyone. STS never checks a password or an MFA code --- that already happened at the external IdP before STS is ever called.
- It does not grant permissions by itself. STS only hands out credentials for a role; what that role can actually do is defined separately by the role's permission policy (out of scope for this doc).
- It is not a long-term credential store. Everything it returns has an expiration, typically 15 minutes to 12 hours.
Where it sits: STS is the trust-translation layer between an external identity system and IAM. Upstream of it sits the IdP (Okta, Azure AD, ADFS, Google, GitHub Actions, Cognito). Downstream of it sits every other AWS API, which accepts the temporary credentials exactly like any other SigV4-signed request.
Two protocols reach STS, depending on what kind of token your IdP hands you:
| Feature | SAML 2.0 Federation | OIDC / Web Identity Federation |
|---|---|---|
| Typical use case | Enterprise workforce SSO | Web/mobile apps, CI/CD (e.g. GitHub Actions) |
| Token type | SAML Assertion (signed XML) | JSON Web Token (JWT) |
| Common providers | Okta, Ping, Azure AD, AD FS | Google, Cognito, GitHub Actions |
| STS API call | AssumeRoleWithSAML |
AssumeRoleWithWebIdentity |
| AWS CLI command | aws sts assume-role-with-saml |
aws sts assume-role-with-web-identity |
For enterprise workforce SSO specifically, AWS recommends IAM Identity Center over hand-rolled SAML federation --- it automates this trust setup across many accounts instead of you maintaining it per-account. See
aws-adfs-assume-principles.mdfor how a raw ADFS/SAML flow looks end to end once it reaches EKS.
3. Concept map: the whole territory
3.1 Domain mapping
| Real-world problem element | System concept | Why this abstraction |
|---|---|---|
| Someone/something needs to prove who they are, without an AWS password | External Identity Provider (IdP) | Authentication happens once, at the source of truth (Okta, GitHub, Google) --- AWS never touches a credential it didn't issue |
| AWS needs to decide "do I trust this issuer at all" | IAM Identity Provider object (saml-provider / oidc-provider) |
A one-time registered trust anchor (certificate or thumbprint) that lets AWS cryptographically verify tokens from that IdP |
| AWS needs to decide "which identities from that IdP may act as this role" | IAM Role Trust Policy | Scopes trust down from "any token this IdP signs" to "only tokens matching this audience/subject," per role |
| The actual hand-off: proof → access | STS API call (AssumeRoleWithSAML / AssumeRoleWithWebIdentity) |
One stateless verification-and-mint step; STS itself stores nothing |
| Access needs to be safe to hand out and forget about | Temporary Security Credentials | Self-expiring AccessKeyId/SecretAccessKey/SessionToken --- a compromised copy is only dangerous until it expires |
| The caller needs to actually use those credentials | Authenticated Session | Credentials loaded into env vars / CLI profile / SDK so every subsequent AWS call is SigV4-signed with them |
3.2 Key players
-
External IdP (Okta, Azure AD, ADFS, Google, GitHub Actions OIDC issuer)
- Owns: authenticating the human or workload and issuing a signed token/assertion.
- Knows: the real identity (user, or CI job identity), its own signing keys, the audience it's issuing for.
- Does not do: know anything about AWS roles or permissions --- it has no idea what AWS is going to do with the token.
-
IAM Identity Provider registration (
arn:...:saml-provider/...orarn:...:oidc-provider/...)- Owns: the cryptographic trust anchor --- the certificate (SAML) or thumbprint/issuer URL (OIDC) AWS uses to validate signatures.
- Knows: issuer metadata, nothing about individual users.
- Does not do: grant any permission itself --- it only makes an IdP nameable as a
Federatedprincipal.
-
IAM Role Trust Policy
- Owns: the decision of which federated principals may call
sts:AssumeRole*on this specific role, and under what conditions (audience, subject claims, branch/repo restrictions, etc). - Knows: the
Federatedprincipal ARN and aConditionblock. - Does not do: define what the role can do once assumed --- that's a separate permissions policy attached to the role.
- Owns: the decision of which federated principals may call
-
AWS STS
- Owns: verifying the presented token/assertion against the registered trust anchor, checking it against the trust policy's conditions, and --- if everything matches --- minting temporary credentials for exactly that role.
- Knows: nothing persistent; it's a stateless verify-then-sign service.
- Does not do: store identities, manage users, or remember past sessions.
-
Temporary Security Credentials / Session
- Owns: representing time-bounded, role-scoped AWS access.
- Knows:
AccessKeyId,SecretAccessKey,SessionToken,Expiration. - Does not do: outlive its expiry or apply to any role other than the one requested.
3.3 Coordination at a glance
AWS Service API AWS STS External IdP User / CI Job AWS Service API AWS STS External IdP User / CI Job #mermaid-svg-KXPLLPMgKW7TxpWN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KXPLLPMgKW7TxpWN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KXPLLPMgKW7TxpWN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KXPLLPMgKW7TxpWN .error-icon{fill:#552222;}#mermaid-svg-KXPLLPMgKW7TxpWN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KXPLLPMgKW7TxpWN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KXPLLPMgKW7TxpWN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KXPLLPMgKW7TxpWN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KXPLLPMgKW7TxpWN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KXPLLPMgKW7TxpWN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KXPLLPMgKW7TxpWN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KXPLLPMgKW7TxpWN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KXPLLPMgKW7TxpWN .marker.cross{stroke:#333333;}#mermaid-svg-KXPLLPMgKW7TxpWN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KXPLLPMgKW7TxpWN p{margin:0;}#mermaid-svg-KXPLLPMgKW7TxpWN .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KXPLLPMgKW7TxpWN text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-KXPLLPMgKW7TxpWN .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KXPLLPMgKW7TxpWN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-KXPLLPMgKW7TxpWN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-KXPLLPMgKW7TxpWN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-KXPLLPMgKW7TxpWN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-KXPLLPMgKW7TxpWN .sequenceNumber{fill:white;}#mermaid-svg-KXPLLPMgKW7TxpWN #sequencenumber{fill:#333;}#mermaid-svg-KXPLLPMgKW7TxpWN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-KXPLLPMgKW7TxpWN .messageText{fill:#333;stroke:none;}#mermaid-svg-KXPLLPMgKW7TxpWN .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KXPLLPMgKW7TxpWN .labelText,#mermaid-svg-KXPLLPMgKW7TxpWN .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-KXPLLPMgKW7TxpWN .loopText,#mermaid-svg-KXPLLPMgKW7TxpWN .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-KXPLLPMgKW7TxpWN .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KXPLLPMgKW7TxpWN .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-KXPLLPMgKW7TxpWN .noteText,#mermaid-svg-KXPLLPMgKW7TxpWN .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-KXPLLPMgKW7TxpWN .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KXPLLPMgKW7TxpWN .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KXPLLPMgKW7TxpWN .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KXPLLPMgKW7TxpWN .actorPopupMenu{position:absolute;}#mermaid-svg-KXPLLPMgKW7TxpWN .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-KXPLLPMgKW7TxpWN .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KXPLLPMgKW7TxpWN .actor-man circle,#mermaid-svg-KXPLLPMgKW7TxpWN line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-KXPLLPMgKW7TxpWN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Verify signature against registered IdP trust anchor + role trust policy conditions Authenticate (SSO / workload identity) 1 Signed token (JWT) or assertion (SAML) 2 AssumeRoleWithWebIdentity / AssumeRoleWithSAML (RoleArn, token) 3 Temporary credentials (AccessKeyId, SecretAccessKey, SessionToken, Expiration) 4 SigV4-signed API request using temp credentials 5 Authorized response (or AccessDenied) 6
Caption: one hand-off --- proof of identity in, time-bounded AWS access out. The failure edge (trust policy condition mismatch → AccessDenied at the STS step) is covered in the trust-policy deep dive below.
4. Running example
Scenario: A GitHub Actions workflow needs to deploy infrastructure to AWS --- no static AWS keys stored in GitHub secrets.
- The workflow run starts. GitHub's OIDC issuer mints a JWT scoped to this exact job (claims include the repo, branch, and workflow).
- The job calls STS (
AssumeRoleWithWebIdentity), presenting that JWT and the targetRoleArn. - STS checks the JWT's signature against the IAM OIDC provider registered for
token.actions.githubusercontent.com, then checks the role's trust policy conditions (audience, and typically thesubclaim restricting this to a specific repo/branch). - STS returns temporary credentials, valid for the rest of the job.
- The workflow step exports them into its session and runs
terraform apply/aws clicommands. - When the job ends, the credentials are seconds from expiring anyway --- nothing to revoke.
This trunk touches every concept above. A second, parallel scenario --- Priya, an engineer, signs into the AWS console via Okta SSO --- walks the identical skeleton with SAML instead of OIDC, and is called out in each dive below as the comparison case.
5. Depth dives
5.1 External IdP: what the token actually looks like
Recap: issues the signed proof in step 1 of the example; nothing here is AWS-specific yet.
- OIDC (GitHub Actions → JWT): a JSON Web Token, base64-encoded, cryptographically signed by GitHub's OIDC issuer. Claims include
aud(audience),sub(e.g.repo:my-org/my-repo:ref:refs/heads/main), and standardiss/exp. - SAML (Okta → assertion): an XML
<samlp:Response>containing a signed<Assertion>, base64-encoded before transport. Its claims are attributes rather than JWT fields, but play the same role.
⚓ Back to the example: in step 1, GitHub's issuer signs a JWT whose sub claim literally encodes "this exact repo, this exact branch, this exact workflow run." That specificity is what step 3's trust policy condition will check against.
Failure behavior: if the IdP's own auth fails (bad AD credentials, MFA challenge, GitHub issuer misconfigured), no token is ever produced --- the flow never reaches STS.
5.2 IAM Identity Provider registration: the trust anchor
Recap: the one-time setup that lets AWS verify signatures from a given IdP at all; referenced implicitly by step 3.
Both variants are registered once per IdP, then reused by every role that wants to trust it:
- OIDC: register the issuer URL + audience + certificate thumbprint → produces an
oidc-providerARN. - SAML: upload the IdP's metadata XML (contains its signing certificate) → produces a
saml-providerARN.
⚓ Back to the example: the Federated principal in the trust policy below (oidc-provider/token.actions.githubusercontent.com) is this registration --- it's what tells AWS "I know how to check signatures from GitHub."
Failure behavior: certificate/thumbprint mismatch (e.g. IdP rotated its signing cert and AWS wasn't updated) → signature validation fails before conditions are even evaluated.
5.3 IAM Role Trust Policy: who may assume this role
Recap: the per-role gate evaluated in step 3 --- narrows "any token this IdP signs" down to "only these tokens."
OIDC / Web Identity (primary example):
json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}
}
}
]
}
SAML (Okta comparison case):
json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:saml-provider/Okta"
},
"Action": "sts:AssumeRoleWithSAML",
"Condition": {
"StringEquals": {
"SAML:aud": "https://signin.aws.amazon.com/saml"
}
}
}
]
}
⚓ Back to the example: the sub condition (repo:my-org/my-repo:ref:refs/heads/main) is exactly why this role can only be assumed by workflow runs on main in that one repo --- a PR from a fork, or a run on another branch, fails this condition and never gets credentials, no matter how valid its JWT signature is.
Failure behavior: this is the single most common federation failure --- signature is valid, but the condition doesn't match (wrong branch, wrong audience). STS returns AccessDenied naming the role, not the condition, which is why mismatched sub/aud values are the first thing to check.
5.4 STS API call: the actual exchange
Recap: step 2--4 of the example --- the stateless verify-and-mint step.
bash
# OIDC / Web Identity
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::123456789012:role/FederatedOidcRole \
--role-session-name "OidcSession" \
--web-identity-token "eyJhbGciOi..."
# SAML
aws sts assume-role-with-saml \
--role-arn arn:aws:iam::123456789012:role/FederatedSamlRole \
--principal-arn arn:aws:iam::123456789012:saml-provider/Okta \
--saml-assertion "PHNhbWxwOkF1dGhuUmVx..."
⚓ Back to the example: GitHub Actions' aws-actions/configure-aws-credentials step wraps this exact assume-role-with-web-identity call --- it fetches the JWT from GitHub's OIDC endpoint and passes it straight through.
Failure behavior: clock skew, expired token, or a trust-policy mismatch (5.3) all surface here as an STS-level error before any AWS resource is ever touched.
5.5 Temporary Credentials & Session: using the output
Recap: step 4--5 --- what the caller actually gets and does with it.
Both API calls return the same shape of credential block:
bash
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="wJalrXUtn..."
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2Vj..."
Any subsequent AWS CLI/SDK call in that shell (or CI step) picks these up automatically and signs requests with them.
⚓ Back to the example: the GitHub Actions job's remaining steps (terraform apply, etc.) run in the same shell environment where these three variables are now set --- no code in those steps needs to know federation happened at all.
Failure behavior: once Expiration passes, every subsequent call fails with ExpiredToken --- by design, there is nothing to revoke because there was never anything long-lived to begin with.
6. Full walkthrough
Tying it together, end to end, for the GitHub Actions run:
- Trigger: a push to
mainstarts the workflow. - IdP (5.1): GitHub's OIDC issuer signs a JWT:
aud=sts.amazonaws.com,sub=repo:my-org/my-repo:ref:refs/heads/main. - STS call (5.4): the job calls
assume-role-with-web-identitywith that JWT andRoleArn=FederatedOidcRole. - Trust verification (5.2 + 5.3): STS checks the JWT's signature against the registered
oidc-provider/token.actions.githubusercontent.comtrust anchor, then checks the role's trust policy --- both theaudandsubconditions match this exact repo/branch. - Credentials minted (5.5): STS returns
AccessKeyId/SecretAccessKey/SessionToken, valid for the remainder of the job. - Session used: the workflow exports them and runs
terraform apply; every AWS API call is signed with these temporary credentials. - Natural expiry: minutes later the job finishes; the credentials expire shortly after --- nothing to clean up, nothing to leak long-term.
AWS APIs AWS STS GitHub OIDC Issuer GitHub Actions Job AWS APIs AWS STS GitHub OIDC Issuer GitHub Actions Job #mermaid-svg-VEecyIitw8MFiFiF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VEecyIitw8MFiFiF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VEecyIitw8MFiFiF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VEecyIitw8MFiFiF .error-icon{fill:#552222;}#mermaid-svg-VEecyIitw8MFiFiF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VEecyIitw8MFiFiF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VEecyIitw8MFiFiF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VEecyIitw8MFiFiF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VEecyIitw8MFiFiF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VEecyIitw8MFiFiF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VEecyIitw8MFiFiF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VEecyIitw8MFiFiF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VEecyIitw8MFiFiF .marker.cross{stroke:#333333;}#mermaid-svg-VEecyIitw8MFiFiF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VEecyIitw8MFiFiF p{margin:0;}#mermaid-svg-VEecyIitw8MFiFiF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VEecyIitw8MFiFiF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-VEecyIitw8MFiFiF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VEecyIitw8MFiFiF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-VEecyIitw8MFiFiF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-VEecyIitw8MFiFiF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-VEecyIitw8MFiFiF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-VEecyIitw8MFiFiF .sequenceNumber{fill:white;}#mermaid-svg-VEecyIitw8MFiFiF #sequencenumber{fill:#333;}#mermaid-svg-VEecyIitw8MFiFiF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-VEecyIitw8MFiFiF .messageText{fill:#333;stroke:none;}#mermaid-svg-VEecyIitw8MFiFiF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VEecyIitw8MFiFiF .labelText,#mermaid-svg-VEecyIitw8MFiFiF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-VEecyIitw8MFiFiF .loopText,#mermaid-svg-VEecyIitw8MFiFiF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-VEecyIitw8MFiFiF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VEecyIitw8MFiFiF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VEecyIitw8MFiFiF .noteText,#mermaid-svg-VEecyIitw8MFiFiF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-VEecyIitw8MFiFiF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VEecyIitw8MFiFiF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VEecyIitw8MFiFiF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VEecyIitw8MFiFiF .actorPopupMenu{position:absolute;}#mermaid-svg-VEecyIitw8MFiFiF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-VEecyIitw8MFiFiF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VEecyIitw8MFiFiF .actor-man circle,#mermaid-svg-VEecyIitw8MFiFiF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-VEecyIitw8MFiFiF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} verify signature vs oidc-provider trust anchor, verify aud + sub vs role trust policy Request OIDC token for this job 1 JWT (aud=sts.amazonaws.com, sub=repo:my-org/my-repo:ref:refs/heads/main) 2 assume-role-with-web-identity(RoleArn, JWT) 3 AccessKeyId / SecretAccessKey / SessionToken (short TTL) 4 terraform apply (SigV4-signed with temp creds) 5 Resources created 6
To sanity-check the resulting identity at any point in the flow:
bash
aws sts get-caller-identity
This closes the loop on the Stage 1 problem: the GitHub Actions job never held a static AWS key, an operator never provisioned or later had to remember to revoke an IAM user, and access was scoped --- by branch, by repo, by minute --- down to exactly the window in which it was needed. The SAML variant (Priya via Okta) runs through the identical six steps, substituting an assertion for a JWT and AssumeRoleWithSAML for AssumeRoleWithWebIdentity --- same trunk, different token.