mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 19:01:42 +00:00
369 lines
16 KiB
Markdown
369 lines
16 KiB
Markdown
---
|
||
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.
|