--- title: "STS Temporary Credentials and AssumeRole" description: "Use AWS STS to issue temporary credentials: assume an IAM role, request a session token, or federate Kubernetes workloads with OIDC web identity on Spinifex." category: "Identity" tags: - sts - iam - temporary credentials - assume role - oidc - security resources: - title: "Spinifex Repository" url: "https://github.com/mulgadc/spinifex" - title: "AWS STS Documentation" url: "https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html" - title: "AWS Temporary Security Credentials" url: "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html" --- # AWS STS Temporary Credentials and AssumeRole > Use AWS STS to issue temporary credentials: assume an IAM role, request a session token, or federate Kubernetes workloads with OIDC web identity on Spinifex. ## Table of Contents - [Overview](#overview) - [Prerequisites](#prerequisites) - [Checking Your Identity](#checking-your-identity) - [Assuming a Role](#assuming-a-role) - [Using Temporary Credentials](#using-temporary-credentials) - [Session Tokens for Your Own User](#session-tokens-for-your-own-user) - [Web Identity Federation (IRSA)](#web-identity-federation-irsa) - [Trust Policy Validation](#trust-policy-validation) - [Troubleshooting](#troubleshooting) --- ## Overview STS (Security Token Service) issues **temporary credentials**: an `ASIA`-prefixed access key, a secret key, and a session token, all of which expire together at a fixed time. Nothing to rotate, nothing to revoke — the credentials simply stop working. Spinifex supports the three main ways to obtain them: - **`assume-role`** — exchange your IAM user credentials for a [role's](/docs/iam-roles-and-instance-profiles) permissions, gated by both the role's trust policy and your own `sts:AssumeRole` grant. - **`get-session-token`** — get a time-boxed copy of your own [IAM user's](/docs/iam-users-and-policies) permissions. - **`assume-role-with-web-identity`** — exchange an OIDC ID token (a Kubernetes ServiceAccount token) for role credentials, with no IAM credentials at all. EC2 instances get role credentials a fourth way, automatically through [IMDS](/docs/imds) — no STS call needed in the guest. ## Prerequisites - A running Spinifex cluster (see [Setting Up Your Cluster](/docs/setting-up-your-cluster)) - AWS CLI configured with the `spinifex` profile: ```bash export AWS_PROFILE=spinifex ``` ## Instructions ## Checking Your Identity `get-caller-identity` returns whoever signed the request — it needs no permissions and never fails for authorisation reasons, which makes it the standard "which credentials am I using?" check: ```bash aws sts get-caller-identity ``` ```json { "UserId": "AIDA1A2B3C4D5E6F7890", "Account": "000000000001", "Arn": "arn:aws:iam::000000000001:user/admin" } ``` Run with temporary role credentials, the ARN switches to the assumed-role form — see [Using Temporary Credentials](#using-temporary-credentials). ## Assuming a Role `assume-role` needs two independent grants, and a missing one denies the call: 1. The target role's **trust policy** must admit your principal. 2. Your own **identity policy** must grant `sts:AssumeRole` on the role's ARN. The second is why `"Principal": {"AWS": "arn:aws:iam::000000000001:root"}` in a trust policy is safe: it delegates the decision to identity policies in that account rather than opening the role to everyone in it. Create a role that trusts an IAM user (see [IAM Roles and Instance Profiles](/docs/iam-roles-and-instance-profiles) for the full role lifecycle): ```bash cat > /tmp/deploy-trust.json << 'EOF' { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::000000000001:user/admin" }, "Action": "sts:AssumeRole" } ] } EOF aws iam create-role --role-name deploy \ --assume-role-policy-document file:///tmp/deploy-trust.json aws iam put-role-policy --role-name deploy --policy-name ec2-read \ --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["ec2:Describe*"],"Resource":"*"}]}' ``` That policy is what the *session* may do. The caller still needs its own grant to make the call at all: ```bash aws iam put-user-policy --user-name admin --policy-name assume-deploy \ --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"sts:AssumeRole","Resource":"arn:aws:iam::000000000001:role/deploy"}]}' ``` Scope `Resource` to the role ARN exactly as `create-role` returned it, path included — a wildcard such as `arn:aws:iam::000000000001:role/*` grants assumption of every role the account's trust policies admit you to. Then assume it: ```bash aws sts assume-role \ --role-arn arn:aws:iam::000000000001:role/deploy \ --role-session-name release-42 ``` ```json { "Credentials": { "AccessKeyId": "ASIA1A2B3C4D5E6F7890", "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "SessionToken": "IQoJb3JpZ2luX2VjE...", "Expiration": "2026-07-03T11:00:00Z" }, "AssumedRoleUser": { "AssumedRoleId": "AROA1A2B3C4D5E6F7890:release-42", "Arn": "arn:aws:sts::000000000001:assumed-role/deploy/release-42" }, "PackedPolicySize": 0 } ``` The session name tags every session so audit logs can tell *who* used the role; it appears in the assumed-role ARN and the `UserId`. If the trust policy does not allow your principal, `assume-role` fails with `AccessDenied` — no matter what IAM permissions you hold. It fails the same way when your own identity policy carries no `sts:AssumeRole` grant on the role, no matter what the trust policy says. ### Session Duration `--duration-seconds` must be between **900** (15 minutes) and the smaller of the role's `MaxSessionDuration` and **43200** (12 hours); the default is **3600** (1 hour). A value outside that window is rejected with `ValidationError`, so to run longer sessions, raise the role's ceiling first: ```bash # 7200 > MaxSessionDuration (3600) → ValidationError aws sts assume-role --role-arn arn:aws:iam::000000000001:role/deploy \ --role-session-name long-build --duration-seconds 7200 aws iam update-role --role-name deploy --max-session-duration 43200 aws sts assume-role --role-arn arn:aws:iam::000000000001:role/deploy \ --role-session-name long-build --duration-seconds 7200 ``` ### Flags That Differ from AWS - `--external-id` and `--source-identity` are accepted and logged but **not enforced** — trust policies cannot carry the `Condition` blocks that would check them (see [Trust Policy Validation](#trust-policy-validation)). Do not rely on an external ID as a security boundary. - `--serial-number` / `--token-code` (MFA) are not supported and return `InvalidParameterValue`. - `--policy` / `--policy-arns` (session policies) are not supported and return `PackedPolicyTooLarge`. ## Using Temporary Credentials Temporary credentials are used exactly like access keys, plus a third value — the session token. Export all three as environment variables (which take precedence over any profile): ```bash export AWS_ACCESS_KEY_ID=ASIA1A2B3C4D5E6F7890 export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY export AWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjE... export AWS_ENDPOINT_URL=https://localhost:9999 export AWS_CA_BUNDLE=/var/lib/spinifex/config/ca.pem export AWS_DEFAULT_REGION=ap-southeast-2 ``` Or store them in a named profile — the same shape as a user profile with `aws_session_token` added: ```bash aws configure set aws_access_key_id ASIA1A2B3C4D5E6F7890 --profile spinifex-deploy aws configure set aws_secret_access_key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY --profile spinifex-deploy aws configure set aws_session_token IQoJb3JpZ2luX2VjE... --profile spinifex-deploy aws configure set region ap-southeast-2 --profile spinifex-deploy aws configure set endpoint_url https://localhost:9999 --profile spinifex-deploy aws configure set ca_bundle /var/lib/spinifex/config/ca.pem --profile spinifex-deploy ``` Requests are then authorised as the assumed role, not your user: ```bash aws sts get-caller-identity # "Arn": "arn:aws:sts::000000000001:assumed-role/deploy/release-42" aws ec2 describe-instances # allowed by the role's ec2-read policy aws iam list-users # AccessDenied — the role grants ec2:Describe* only ``` When the expiration time passes, every call fails until you fetch fresh credentials — there is no refresh; run `assume-role` again. ## Session Tokens for Your Own User `get-session-token` issues temporary credentials with your **own user's** permissions — no role, no trust policy. Useful for handing a build script credentials that expire on their own instead of your long-lived access key: ```bash aws sts get-session-token --duration-seconds 3600 ``` ```json { "Credentials": { "AccessKeyId": "ASIA1A2B3C4D5E6F7890", "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "SessionToken": "IQoJb3JpZ2luX2VjE...", "Expiration": "2026-07-03T11:00:00Z" } } ``` Duration runs from **900** to **129600** seconds (36 hours), defaulting to **43200** (12 hours). Unlike `assume-role`, out-of-range values are **clamped, not rejected** — asking for 200000 seconds quietly returns a 36-hour session. Use the credentials exactly as in [Using Temporary Credentials](#using-temporary-credentials); `get-caller-identity` keeps reporting your user ARN, since no role is involved. MFA (`--serial-number` / `--token-code`) is not supported and returns `InvalidParameterValue`. ## Web Identity Federation (IRSA) `assume-role-with-web-identity` exchanges an OIDC ID token for role credentials — the mechanism behind **IRSA** (IAM Roles for Service Accounts), where a Kubernetes pod's ServiceAccount token becomes its cloud identity. The call is anonymous: the JWT is the identity, so no IAM credentials sign the request. In Spinifex the token issuer is an [EKS](/docs/eks) cluster. Each cluster publishes a signing-key set (JWKS) under an issuer URL of the form `https://{host}/oidc/eks/{region}/{account-id}/{cluster-name}`, and STS verifies tokens against it directly — no external identity provider is contacted. ### Register the OIDC Provider The role account must register the issuer as an IAM OIDC provider before any token from it is accepted: ```bash aws iam create-open-id-connect-provider \ --url https://spinifex.example.com/oidc/eks/ap-southeast-2/000000000001/prod \ --client-id-list sts.amazonaws.com \ --thumbprint-list 9e99a48a9960b14926bb7f3b02e22da2b0ab7280 ``` ```json { "OpenIDConnectProviderArn": "arn:aws:iam::000000000001:oidc-provider/spinifex.example.com/oidc/eks/ap-southeast-2/000000000001/prod", "Tags": [] } ``` Manage providers with the usual verbs — `get-open-id-connect-provider` and `list-open-id-connect-provider-tags` take the ARN, and tags work like every other IAM resource: ```bash aws iam list-open-id-connect-providers aws iam get-open-id-connect-provider \ --open-id-connect-provider-arn arn:aws:iam::000000000001:oidc-provider/spinifex.example.com/oidc/eks/ap-southeast-2/000000000001/prod aws iam delete-open-id-connect-provider \ --open-id-connect-provider-arn arn:aws:iam::000000000001:oidc-provider/spinifex.example.com/oidc/eks/ap-southeast-2/000000000001/prod ``` ### The IRSA Trust Policy The role's trust policy names the provider as a `Federated` principal and pins the token's `sub` (the ServiceAccount) and `aud` claims with `StringEquals` — the one place a `Condition` block is allowed: ```bash cat > /tmp/irsa-trust.json << 'EOF' { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::000000000001:oidc-provider/spinifex.example.com/oidc/eks/ap-southeast-2/000000000001/prod" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "spinifex.example.com/oidc/eks/ap-southeast-2/000000000001/prod:sub": "system:serviceaccount:default:app", "spinifex.example.com/oidc/eks/ap-southeast-2/000000000001/prod:aud": "sts.amazonaws.com" } } } ] } EOF aws iam create-role --role-name irsa-app \ --assume-role-policy-document file:///tmp/irsa-trust.json ``` The condition keys are the issuer URL (scheme stripped) followed by `:sub` or `:aud`. Only these two keys and only `StringEquals` are accepted — anything wider (`StringLike`, wildcards, other keys) is rejected at `create-role` with `MalformedPolicyDocument` rather than silently over-granting. ### Exchange the Token ```bash aws sts assume-role-with-web-identity \ --role-arn arn:aws:iam::000000000001:role/irsa-app \ --role-session-name pod-app-7f9c4 \ --web-identity-token "$(cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token)" ``` The response has the same `Credentials` block as `assume-role`, plus the verified `SubjectFromWebIdentityToken`, `Provider`, and `Audience`. Duration runs 900–43200 seconds (default 3600), independent of the role's `MaxSessionDuration`. For a token to be accepted it must be an ES256-signed JWT whose `iss` matches a registered OIDC provider in the role's account, whose `aud` contains `sts.amazonaws.com`, whose signature verifies against the cluster's published JWKS, and which has not expired — any failure returns `InvalidIdentityToken`. In-cluster, none of this is manual: pods with an annotated ServiceAccount get the token mounted and the AWS SDK performs the exchange automatically. ## Trust Policy Validation Spinifex validates trust policies (`AssumeRolePolicyDocument`) more strictly than AWS, rejecting at write time with `MalformedPolicyDocument`: - `Condition` blocks anywhere **except** the `StringEquals` IRSA form on `sts:AssumeRoleWithWebIdentity` described above. This is why `--external-id` cannot be enforced. - `NotPrincipal` and `NotAction` elements. - Empty `Principal` blocks and empty-string `Action` elements. Everything the validator accepts is evaluated at assume time; explicit `Deny` statements win, and a policy that matches no statement denies. ## Troubleshooting ### AccessDenied When Assuming a Role Either gate can produce this. A denial from your own identity policy names the principal, action and resource: ``` User: arn:aws:iam::000000000001:user/admin is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::000000000001:role/deploy ``` A bare `AccessDenied` means the role's trust policy does not allow your principal, or the role does not exist — a missing role is deliberately indistinguishable from a denied one, matching AWS. Check who you are, that the role exists, what it trusts, and what you are granted: ```bash aws sts get-caller-identity aws iam list-roles --query 'Roles[].Arn' aws iam get-role --role-name deploy \ --query 'Role.AssumeRolePolicyDocument' ``` For web identity, `AccessDenied` also covers a trust-policy `Condition` that doesn't match the token's `sub`/`aud` claims. ### ValidationError on Duration The requested `--duration-seconds` is outside 900–min(`MaxSessionDuration`, 43200). Check and raise the role's ceiling: ```bash aws iam get-role --role-name deploy --query 'Role.MaxSessionDuration' aws iam update-role --role-name deploy --max-session-duration 43200 ``` Note `get-session-token` never returns this — it clamps instead. ### InvalidParameterValue You passed an MFA flag (`--serial-number` / `--token-code`) — MFA is not supported. `--tags` and `--transitive-tag-keys` on `assume-role` return the same error. ### PackedPolicyTooLarge You passed `--policy` or `--policy-arns`. Session policies are not supported — scope the role's own permissions instead. ### InvalidIdentityToken The web identity token failed verification: malformed JWT, wrong signature algorithm (only ES256 is accepted), expired, `aud` missing `sts.amazonaws.com`, issuer not registered as an OIDC provider in the role's account, or the cluster's JWKS is unavailable. Confirm the provider is registered: ```bash aws iam list-open-id-connect-providers ``` ### Credentials Suddenly Rejected Temporary credentials expired. Check the `Expiration` from the original response and mint a fresh set — sessions cannot be renewed or extended.