Fixing AWS STS InvalidIdentityToken: DecodeAuthorizationMessage Failing on EC2 Instance Profile Credentials
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–30 mins
TL;DR
- What broke:
sts:DecodeAuthorizationMessageis returningInvalidIdentityTokenbecause either the temporary STS token from the EC2 instance profile is expired/malformed, the IAM role lacks explicitsts:DecodeAuthorizationMessagepermission, or you're passing the encoded message as the token instead of the rawencodedMessagefield from the original access-denied response. - How to fix it: Refresh the instance profile credentials via IMDSv2, verify the IAM role attached to the instance has
sts:DecodeAuthorizationMessageexplicitly allowed, and confirm you are passing theencodedMessagestring — not the bearer token — to the API call. - Shortcut: Use our Client-Side Sandbox above to paste your IAM policy and encoded message; it will auto-diagnose the permission gap and refactor your policy without sending your ARNs or tokens to any server.
The Incident (What Does the Error Mean?)
You called aws sts decode-authorization-message --encoded-message <value> from an EC2 instance and received:
An error occurred (InvalidIdentityToken) when calling the DecodeAuthorizationMessage operation:
The identity token included in the request is invalid.
Immediate consequence: You cannot decode the original access-denied authorization message, which means the root cause of your actual permission failure remains completely opaque. This is a double-failure: the primary action is blocked, and the diagnostic tool is also blocked. Production debugging time compounds rapidly.
This error surfaces in three distinct scenarios:
- Expired token: The STS temporary credentials fetched from
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>have passed theirExpirationtimestamp. The SDK has not yet rotated them, or credential caching is broken. - Missing IAM permission: The instance role's attached policy does not include
sts:DecodeAuthorizationMessage. STS requires this as an explicit allow — it is not inherited from any wildcard on other services. - Wrong input value: The
--encoded-messageparameter must receive theencodedfield from the originalAccessDeniederror response body, not the session token itself. Confusing these two strings is the most common operator error.
The Attack Vector / Blast Radius
Why this matters beyond a debugging annoyance:
Temporary credentials from EC2 instance profiles are the primary lateral movement vector in AWS compromise scenarios. When InvalidIdentityToken is thrown repeatedly and silently swallowed by application code, engineers frequently respond by broadening IAM permissions to "fix" the underlying access-denied error without understanding what it actually was. This is how sts:* or iam:* wildcards get introduced into production roles.
Specifically:
- Token expiry not caught in code means your application is running with stale credentials. If the credential refresh loop is broken (e.g., IMDSv2 hop limit set to
1on a containerized workload inside EC2), the instance silently loses its identity. Any attacker who has already obtained a copy of the expired token cannot reuse it — but your application is now effectively unauthenticated and will fail open or fail in unpredictable ways. - If
sts:DecodeAuthorizationMessageis over-granted (e.g., attached to a role accessible by untrusted workloads), any principal with that role can decode authorization messages for any encoded message they intercept, leaking your account's IAM policy structure, resource ARNs, and action boundaries to an attacker performing reconnaissance. - Cross-account confusion: If the EC2 instance assumes a role in another account via
sts:AssumeRoleand then tries to callDecodeAuthorizationMessage, the decoded message context is evaluated against the target account's policies. Misconfigured trust policies here causeInvalidIdentityTokenbecause the token issuer does not match the expected partition/account in the signed token.
How to Fix It (The Solution)
Step 1 — Verify Token Freshness via IMDSv2
SSH to the instance and manually fetch credentials. Check the Expiration field:
# Always use IMDSv2 (IMDSv1 is deprecated and a SSRF risk)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/<YOUR_ROLE_NAME>
If Expiration is in the past, the credential rotation daemon is broken. Check if the instance has outbound HTTPS to sts.<region>.amazonaws.com and that no SCPs are blocking sts:AssumeRole on the instance role itself.
Step 2 — Fix the IAM Policy (The Core Fix)
Bad policy — missing sts:DecodeAuthorizationMessage:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3ReadAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-bucket",
"arn:aws:s3:::my-app-bucket/*"
]
- }
+ },
+ {
+ "Sid": "AllowSTSDecodeAuthorizationMessage",
+ "Effect": "Allow",
+ "Action": "sts:DecodeAuthorizationMessage",
+ "Resource": "*"
+ }
]
}
⚠️ Note:
sts:DecodeAuthorizationMessagedoes not support resource-level restrictions —"Resource": "*"is required and is not a wildcard risk in this specific context. AWS does not allow scoping this action to a specific ARN.
Step 3 — Confirm You Are Passing the Correct String
Wrong — passing the session token:
- aws sts decode-authorization-message \
- --encoded-message "AQoDYXdzEJr..." # This is your SessionToken, NOT the encoded message
Correct — passing the encoded authorization message from the AccessDenied error:
+ # First, capture the encodedMessage from the failing API call:
+ aws s3 ls s3://my-restricted-bucket 2>&1 | grep -o 'encoded message: .*'
+
+ # Then decode it:
+ aws sts decode-authorization-message \
+ --encoded-message "<THE_ENCODED_STRING_FROM_THE_ACCESS_DENIED_ERROR>"
The encoded message is the long base64-like string in the AccessDenied error body under the key encoded. It is completely different from SessionToken.
Enterprise Best Practice — Scope with IAM Condition Keys and Attach via Permission Boundary
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSTSDecodeAuthorizationMessageWithCondition",
"Effect": "Allow",
"Action": "sts:DecodeAuthorizationMessage",
"Resource": "*",
+ "Condition": {
+ "StringEquals": {
+ "aws:RequestedRegion": "us-east-1"
+ },
+ "Bool": {
+ "aws:SecureTransport": "true"
+ }
+ }
}
]
}
- Attach this as a separate managed policy (not inline) so it can be audited, versioned, and reused across roles.
- Apply a Permission Boundary to the instance role that explicitly excludes
iam:*andsts:AssumeRoleto any arbitrary account — this prevents the role from being used for privilege escalation even if the trust policy is misconfigured. - Never attach
sts:DecodeAuthorizationMessageto roles used by Lambda@Edge or cross-region replication — those contexts have different token issuers and will produce the sameInvalidIdentityTokenerror for structural reasons.
💡 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
Stop this from reaching production in the first place.
1. Checkov — Scan IAM Policies in Terraform Before terraform apply
checkov -d ./terraform --check CKV_AWS_109,CKV_AWS_110,CKV_AWS_111
Add a custom Checkov check to flag any EC2 instance profile role that references sts:AssumeRole without a corresponding sts:DecodeAuthorizationMessage allow — these always travel together in debuggable production systems.
2. OPA/Conftest Policy — Enforce in Terraform Plan
# policy/sts_decode_required.rego
package aws.iam
deny[msg] {
role := input.resource_changes[_]
role.type == "aws_iam_role"
not has_decode_permission(role)
msg := sprintf(
"Role '%v' is attached to an EC2 instance profile but lacks sts:DecodeAuthorizationMessage. Add it as a managed policy.",
[role.address]
)
}
has_decode_permission(role) {
policy := role.change.after.inline_policy[_]
contains(policy.policy, "sts:DecodeAuthorizationMessage")
}
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json --policy policy/
3. AWS Config Rule — Continuous Compliance
Deploy the managed AWS Config rule iam-policy-no-statements-with-admin-access and supplement it with a custom Lambda-backed Config rule that:
- Evaluates all EC2 instance profile roles on change
- Flags roles that have
s3:*,ec2:*, or other broad actions without a correspondingsts:DecodeAuthorizationMessageallow (indicating the role was likely broadened to avoid debugging the original error)
4. CloudTrail Alert — Detect Token Expiry Loops
{
"source": ["aws.sts"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["DecodeAuthorizationMessage"],
"errorCode": ["InvalidIdentityToken"]
}
}
Route this EventBridge rule to an SNS topic. More than 3 occurrences per hour from the same sourceIPAddress (your EC2 instance's private IP) means the credential rotation loop is broken and the instance is running degraded.
5. IMDSv2 Enforcement via SCP
{
"Sid": "DenyIMDSv1",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
}
Apply this SCP at the OU level. IMDSv1 is the primary reason credential refresh silently fails in containerized workloads on EC2 — the hop limit of 1 blocks the metadata call from inside the container, causing the SDK to serve expired cached credentials indefinitely.