RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
AWS STS InvalidIdentityToken Decoder
02. Root Cause Vector

EC2 instance profile temporary credentials fail sts:DecodeAuthorizationMessage with InvalidIdentityToken when the token is expired, malfo...

03. Rapid Prescription

Check explicit denies in SCPs, verify STS trust relationships, and audit IAM policy grammar.

Zero-Trust Client IDE/ Security

AWS STS InvalidIdentityToken Decoder

EC2 instance profile temporary credentials fail sts:DecodeAuthorizationMessage with InvalidIdentityToken when the token is expired, malformed, or the calling identity lacks sts:DecodeAuthorizationMessage permission.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(45L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash
# TODO: Migrate this to Terraform ASAP. Current IAM policy is a mess.
# Legacy PCI compliance wrapper for EC2 instance profile - DO NOT MODIFY WITHOUT APPROVAL
# VULNERABILITY: The STS call uses a hardcoded region that mismatches the instance metadata's token region, causing decode failures.

# Load legacy config from secure S3 bucket (simulated)
LEGACY_CONFIG="s3://pci-secure-zone/legacy-bootstrap/env.sh"
# FAKE AWS KEYS (from old migration script, should be overridden by instance profile)
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# Stripe test key left from payment microservice PoC
export STRIPE_API_KEY="sk_test_4eC39HqLyjWDarjtT1zdp7dc"

# Multi-line RSA private key for internal service auth (should be in Secrets Manager)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArGJ7R1vL7wKvZ6pK7z6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X\n6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X\n6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X6X\n-----END RSA PRIVATE KEY-----"

# Fetch instance metadata token (IMDSv2)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" 2>/dev/null)
ROLE_NAME=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/ 2>/dev/null)
CREDS_JSON=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" "http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE_NAME" 2>/dev/null)

# Extract temporary credentials
ACCESS_KEY=$(echo "$CREDS_JSON" | grep -o '"AccessKeyId" : "[^"]*"' | cut -d'"' -f4)
SECRET_KEY=$(echo "$CREDS_JSON" | grep -o '"SecretAccessKey" : "[^"]*"' | cut -d'"' -f4)
SESSION_TOKEN=$(echo "$CREDS_JSON" | grep -o '"Token" : "[^"]*"' | cut -d'"' -f4)

# Override with instance profile creds (but region is mismatched due to legacy hardcode)
export AWS_ACCESS_KEY_ID="$ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="$SECRET_KEY"
export AWS_SESSION_TOKEN="$SESSION_TOKEN"
# Hardcoded region from old deployment pipeline - conflicts with token's implicit region
export AWS_DEFAULT_REGION="us-west-2"  # TODO: This should be dynamic based on instance placement

# PostgreSQL DB URI for legacy reporting (should be encrypted)
DB_URI="postgresql://legacy_user:SuperSecretPass123!@pci-db.internal:5432/audit_logs"

# Attempt to decode an authorization message (simulating a security scan tool)
# The STS client will use us-west-2, but the token is issued for the instance's actual region (e.g., us-east-1)
aws sts decode-authorization-message --encoded-message "FAKEENCODEDMESSAGE" --profile legacy-profile 2>&1 | grep -i "InvalidIdentityToken"
# Expected error: An error occurred (InvalidIdentityToken) when calling the DecodeAuthorizationMessage operation: The security token included in the request is invalid.

# Old commented-out fix attempt
# export AWS_REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/region)
# This was reverted due to "network latency issues in PCI zone"
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

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:DecodeAuthorizationMessage is returning InvalidIdentityToken because either the temporary STS token from the EC2 instance profile is expired/malformed, the IAM role lacks explicit sts:DecodeAuthorizationMessage permission, or you're passing the encoded message as the token instead of the raw encodedMessage field 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:DecodeAuthorizationMessage explicitly allowed, and confirm you are passing the encodedMessage string — 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.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

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:

  1. Expired token: The STS temporary credentials fetched from http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name> have passed their Expiration timestamp. The SDK has not yet rotated them, or credential caching is broken.
  2. 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.
  3. Wrong input value: The --encoded-message parameter must receive the encoded field from the original AccessDenied error response body, not the session token itself. Confusing these two strings is the most common operator error.

▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

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 1 on 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:DecodeAuthorizationMessage is 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:AssumeRole and then tries to call DecodeAuthorizationMessage, the decoded message context is evaluated against the target account's policies. Misconfigured trust policies here cause InvalidIdentityToken because 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:DecodeAuthorizationMessage does 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:* and sts:AssumeRole to any arbitrary account — this prevents the role from being used for privilege escalation even if the trust policy is misconfigured.
  • Never attach sts:DecodeAuthorizationMessage to roles used by Lambda@Edge or cross-region replication — those contexts have different token issuers and will produce the same InvalidIdentityToken error for structural reasons.

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

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 corresponding sts:DecodeAuthorizationMessage allow (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.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does sts:DecodeAuthorizationMessage require Resource: * and can I restrict it further?
This is a fundamental AWS design constraint: the STS service does not support resource-level permissions for DecodeAuthorizationMessage, forcing you to use 'Resource': '*' in your IAM policy. While you can add compensating controls like aws:SecureTransport, manually crafting these condition keys is fragile and often incomplete, leaving a subtle but critical permissions gap. To eliminate this risk, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix with precise, enforceable guardrails.
Q2.The EC2 instance has the correct IAM policy but I still get InvalidIdentityToken — what else could cause this?
The root cause is often a hidden infrastructure layer—like a container with a misconfigured IMDSv2 hop limit—silently serving expired credentials. Manually diagnosing this requires deep, cross-service expertise and risks misconfiguration that expands the security blast radius. Instead of guessing, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that isolates and remediates the exact token validation failure.
Q3.Is it safe to log the output of sts:DecodeAuthorizationMessage to CloudWatch Logs for post-incident analysis?
Manually securing this log data is a compliance nightmare at scale. The decoded message contains sensitive IAM context and resource ARNs—a high-value target. While you can attempt encryption and strict access policies, doing this consistently across large mono-repos or enterprise deployments is error-prone and fails audits. For automated pull request generation with Maker-Checker review, upgrade to the StackEngine CI/CD Automator (Team Edition) to enforce zero-trust logging pipelines and eliminate compliance gaps.