Initializing Enclave...

How to Fix AWS CLI v2 AssumeRole 'The security token included in the request is invalid' After Session Expiry

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins


TL;DR

  • What broke: Your STS-issued session token (via AssumeRole) hit its MaxSessionDuration or the 1-hour default TTL. Every subsequent CLI call using that cached token returns InvalidClientTokenId or ExpiredTokenException.
  • How to fix it: Force a token refresh via aws sts assume-role manually, or configure your ~/.aws/config profile with credential_process / role_arn + source_profile so the CLI auto-refreshes.
  • Fast path: Use our Client-Side Sandbox above to auto-refactor your credentials config or shell wrapper — paste your config, get corrected output instantly.

The Incident (What Does the Error Mean?)

You ran a CLI command and got this:

An error occurred (InvalidClientTokenId) when calling the GetCallerIdentity operation:
The security token included in the request is invalid.

or this variant:

An error occurred (ExpiredTokenException) when calling the AssumeRole operation:
Token included in the request is expired.

Immediate consequence: Every AWS API call authenticated via that profile is dead. If this is a deployment pipeline, your CI/CD run is blocked. If it's a production ops script, your on-call engineer cannot execute remediation commands. The credentials cache at ~/.aws/cli/cache/ or the AWS_SESSION_TOKEN env var is holding a stale token that STS is rejecting at the signature validation layer.


The Attack Vector / Blast Radius

This is not just an inconvenience — stale token handling is a security boundary failure with real blast radius:

  1. Hardcoded or long-lived tokens in CI/CD env vars — When engineers "fix" this by extending MaxSessionDuration to 12 hours or exporting static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN into environment variables, those tokens become long-lived secrets. A compromised CI runner, a leaked .env file, or a printenv in build logs exposes a fully-authenticated AWS session.

  2. Cache poisoning window — The CLI cache directory (~/.aws/cli/cache/*.json) stores plaintext JSON with AccessKeyId, SecretAccessKey, and SessionToken. If an attacker has filesystem read access, they can exfiltrate a valid token before expiry. Extending TTL directly extends the exfiltration window.

  3. Cascading pipeline failures — In multi-stage pipelines where stage 2 assumes a role from stage 1's token, a single expiry mid-pipeline causes all downstream stages to fail with cryptic errors, often misdiagnosed as permission errors rather than token expiry.

  4. Silent failures in SDKs — Boto3 and other SDKs may silently retry and surface a generic ClientError, masking the real cause and delaying diagnosis by 20–40 minutes in a production incident.


How to Fix It (The Solution)

Basic Fix — Force Refresh the Token Manually

Nuke the CLI cache and re-assume the role:

# Clear stale cached credentials
rm -f ~/.aws/cli/cache/*.json

# Re-assume the role and export fresh tokens
OUTPUT=$(aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/MyRole \
  --role-session-name refresh-session \
  --duration-seconds 3600)

export AWS_ACCESS_KEY_ID=$(echo $OUTPUT | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $OUTPUT | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $OUTPUT | jq -r '.Credentials.SessionToken')

This gets you unblocked in under 2 minutes. It is not a permanent fix.


Enterprise Best Practice — Auto-Refreshing Profile via ~/.aws/config

The correct fix is to let the AWS CLI v2 credential provider chain handle refresh automatically using profile chaining. Never hardcode session tokens.

# ~/.aws/config

- [profile broken-profile]
- aws_access_key_id = ASIAXXXXXXXXXXXXXXXXXXX
- aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- aws_session_token = AQoXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX  # HARDCODED — EXPIRES AND BREAKS

+ [profile base-identity]
+ credential_process = aws-vault exec base-identity --json
+ # OR if using IAM user long-term keys as source:
+ # aws_access_key_id = AKIAIOSFODNN7EXAMPLE
+ # aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
+
+ [profile my-role]
+ role_arn = arn:aws:iam::123456789012:role/MyRole
+ source_profile = base-identity
+ role_session_name = cli-auto-refresh
+ duration_seconds = 3600
+ # CLI v2 will auto-refresh this token before expiry when using this profile

Usage after fix:

aws s3 ls --profile my-role
# CLI v2 automatically calls sts:AssumeRole and caches/refreshes the token

For CI/CD (GitHub Actions / GitLab CI) — use OIDC, not static tokens:

- env:
-   AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
-   AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-   AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }}  # Rotates every N hours, breaks pipeline

+ permissions:
+   id-token: write
+   contents: read
+
+ - name: Configure AWS Credentials
+   uses: aws-actions/configure-aws-credentials@v4
+   with:
+     role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
+     aws-region: us-east-1
+     # OIDC — no static secrets, no expiry surprises

💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. Checkov — Detect Hardcoded Session Tokens in IaC

Checkov rule CKV_AWS_46 flags hardcoded credentials. Add to your pipeline:

- name: Checkov IaC Scan
  run: checkov -d . --check CKV_AWS_46 --compact

2. OPA / Conftest Policy — Block Static Token Injection

# policy/no_static_session_token.rego
package main

deny[msg] {
  input.env[key]
  key == "AWS_SESSION_TOKEN"
  msg := "Static AWS_SESSION_TOKEN in pipeline env is forbidden. Use OIDC or instance role."
}
conftest test pipeline.yaml --policy policy/

3. aws-vault for Local Development

Replace all local ~/.aws/credentials static key usage with aws-vault, which stores credentials in the OS keychain and auto-rotates STS tokens:

brew install aws-vault
aws-vault add base-identity
aws-vault exec my-role -- aws s3 ls

4. Set a Token Expiry Monitor (CloudWatch + EventBridge)

For long-running assumed-role sessions in ECS tasks or EC2, configure a CloudWatch alarm on aws.sts GetCallerIdentity 4xx error rates. An uptick in InvalidClientTokenId errors at a predictable interval is a leading indicator of token refresh logic failure — catch it before it pages your on-call.

5. Enforce MaxSessionDuration via SCP

Do not set MaxSessionDuration above 4 hours without a documented justification. Use an SCP to enforce this org-wide:

{
  "Effect": "Deny",
  "Action": "iam:UpdateRole",
  "Resource": "*",
  "Condition": {
    "NumericGreaterThan": {
      "iam:MaxSessionDuration": "14400"
    }
  }
}

Longer sessions are not a fix for broken refresh logic — they are a security liability masquerading as a convenience.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →