Temporary credentials expire. That is the point of them. ExpiredToken means the STS session behind your call has passed its lifetime.
| Credential source | Default lifetime |
|---|---|
| IAM Identity Center (SSO) | 8 hours, configurable |
sts:AssumeRole | 1 hour, up to the role's maximum |
GetSessionToken with MFA | 12 hours |
| EC2 instance profile | Rotated automatically |
| ECS task role | Rotated automatically |
| IRSA on EKS | Rotated automatically |
The last three refresh themselves, so if you are seeing this on an instance, something is bypassing the metadata service.
Which source are you actually using?
aws configure list
Name Value Type Location
access_key ****************WXYZ env AWS_ACCESS_KEY_ID
secret_key ****************abcd env AWS_SECRET_ACCESS_KEY
region eu-west-1 config-file ~/.aws/config
Type env is the answer in a large share of cases. Environment variables win over everything, so an AWS_SESSION_TOKEN exported this morning shadows the profile you just refreshed, and re-running aws sso login appears to do nothing.
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
The precedence order, highest first: environment variables, then the CLI's --profile, then AWS_PROFILE, then the default profile, then container credentials, then the instance metadata service.
Refresh it
# IAM Identity Center
aws sso login --profile prod
# Manually assumed role
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/deploy \
--role-session-name deploy-$(date +%s)
Confirm before retrying the real thing:
aws sts get-caller-identity --profile prod
Stop assuming roles by hand
Configure the profile to do it, and the SDK refreshes automatically when the session expires:
# ~/.aws/config
[profile prod]
sso_session = acme
sso_account_id = 123456789012
sso_role_name = Developer
region = eu-west-1
[profile deploy]
source_profile = prod
role_arn = arn:aws:iam::123456789012:role/deploy
duration_seconds = 3600
[sso-session acme]
sso_start_url = https://acme.awsapps.com/start
sso_region = eu-west-1
sso_registration_scopes = sso:account:access
Now aws --profile deploy s3 ls assumes the role when needed and renews it silently. Exporting the output of assume-role into environment variables is what creates a credential that goes stale in your shell with nothing to refresh it.
Longer sessions
The default assume-role duration is one hour. Raise the role's maximum, then ask for it:
aws iam update-role --role-name deploy --max-session-duration 14400
duration_seconds = 14400
Note role chaining caps at one hour regardless of the maximum. If you assume role A and then assume role B from A, the second session cannot exceed an hour and duration_seconds above that is rejected. This surprises people building nested assume-role setups for a long deployment.
RequestExpired is a different thing
An error occurred (RequestExpired): Request has expired.
This is clock skew, not an expired session. Every signed AWS request carries a timestamp, and the service rejects anything more than five minutes out.
timedatectl status
sudo systemctl restart systemd-timesyncd
Long-suspended laptops and VMs restored from snapshots are the usual sources. Inside a container, the clock comes from the host, so fix it there.
In CI, use OIDC
Static access keys in CI secrets are the thing this error should push you away from. Every major CI platform can exchange an OIDC token for a short-lived AWS role session, with no stored credentials at all:
# GitHub Actions
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: eu-west-1
Credentials are minted per job and expire when it ends. Nothing to rotate, nothing to leak.
Terraform and the SDK
Error: error configuring S3 Backend: no valid credential sources found
Terraform reads the same chain. It also caches backend credentials at init time, so a very long apply can outlive the session it started with. For long applies, raise duration_seconds rather than re-running init partway through.
A checklist
aws configure listand read the Type column.Type env→unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN.aws sso login --profile <p>, thenaws sts get-caller-identityto confirm.- Configure
role_arnandsource_profileso the SDK refreshes for you. - Need longer → raise the role's
--max-session-durationand setduration_seconds. - Role chaining is capped at one hour whatever you configure.
RequestExpiredis clock skew. Check NTP.- In CI, replace static keys with OIDC.
Frequently Asked Questions
Why does the error persist after I log in again?
Because stale environment variables are overriding your refreshed profile. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN sit at the top of the credential chain, so an AWS_SESSION_TOKEN exported hours ago shadows whatever aws sso login just wrote. aws configure list shows the source of each value in its Type column. Unset all three and try again; this single step resolves most cases where a re-login appears to have had no effect.
What is the difference between ExpiredToken and RequestExpired?
ExpiredToken means the STS session behind your credentials has passed its lifetime and you need new credentials. RequestExpired means the signed request's timestamp is more than five minutes away from AWS's clock, so it is a time synchronisation problem on your machine rather than a credential one. Check with timedatectl status and restart your NTP service. Laptops resumed from long suspends and VMs restored from snapshots are the usual causes.
How do I get sessions that last longer than an hour?
Raise the role's maximum with aws iam update-role --role-name <name> --max-session-duration 14400, then request it via duration_seconds in the profile or --duration-seconds on the CLI call. The important exception is role chaining: if you assume one role and then assume a second from it, the second session is capped at one hour regardless of the role's maximum, and asking for more is rejected outright. Restructuring to assume the target role directly avoids that cap.
Why do my EC2 instances never hit this?
Because an instance profile's credentials are delivered by the instance metadata service and the SDK refreshes them automatically before they expire. The same is true of ECS task roles and IRSA on EKS. If you do see ExpiredToken on an instance, something is bypassing that mechanism, almost always hardcoded environment variables or a credentials file baked into an AMI. Remove them and let the metadata service supply credentials.
What is the best way to handle AWS credentials in CI?
OIDC federation, which every major CI platform now supports. The platform issues a signed identity token for the job, AWS exchanges it for a short-lived role session, and no long-lived access key is stored anywhere. Credentials are minted per job and expire when it finishes, so there is nothing to rotate and nothing useful to leak. It also gives you per-workflow conditions in the trust policy, so a role can be restricted to one repository and even one branch.