How to Fix 'Invalid Principal in Policy' in AWS IAM Trust Policies
Threat/Impact Level: CRITICAL | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: AWS rejected the trust policy because a
Principalelement contains an ARN or identifier that doesn't exist, is malformed, or references a deleted IAM entity. - How to fix it: Validate every ARN in the
Principalblock — correct account IDs, service DNS suffixes, and ensure referenced users/roles actually exist in the account. - Fast path: Use our Client-Side Sandbox above to paste your trust policy and auto-refactor the invalid principals instantly.
The Incident (What Does the Error Mean?)
Raw error from AWS Console or CLI:
MalformedPolicyDocument: Invalid principal in policy: "arn:aws:iam::123456789012:role/NonExistentRole"
or via Terraform:
Error: error creating IAM Role: MalformedPolicyDocument:
Invalid principal in policy: "arn:aws:sts::ACCOUNT_ID:assumed-role/MyRole/session"
Immediate consequence: The role creation or update is hard-rejected by the IAM API. No partial state. If this is a Terraform apply, the resource is never written. If it's a live policy update, the existing trust policy remains unchanged — meaning whatever was broken before is still broken. CI/CD pipelines calling sts:AssumeRole fail with AccessDenied until this is resolved.
The Attack Vector / Blast Radius
This isn't just a config annoyance — a misconfigured trust policy has two distinct blast radii:
Scenario A — Overly permissive principal that slips through (e.g., wildcard *):
If you attempt "Principal": "*" without a restrictive Condition block, any authenticated AWS principal in any account can attempt sts:AssumeRole. A threat actor who discovers the role ARN via CloudTrail enumeration or a public S3 bucket policy can assume it and inherit all attached permissions. This is a full account takeover vector if the role has AdministratorAccess.
Scenario B — Deleted principal left in trust policy:
When an IAM user or role is deleted, AWS converts its ARN in existing trust policies to the underlying AIDAXXX principal ID. AWS then rejects any new policy containing that stale ID. This is the most common production trigger — an engineer deletes a service account without auditing which roles trusted it.
Cascading failure: In EKS/IRSA setups, a broken trust policy on a node role or pod identity role means kubelets can't pull ECR images, pods can't access Secrets Manager, and the entire workload silently degrades within minutes of a node recycle.
How to Fix It (The Solution)
Basic Fix — Correct the Malformed ARN
Most common mistakes: wrong account ID, typo in service suffix, referencing sts instead of iam for role principals.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
- "AWS": "arn:aws:iam::ACCOUNT_ID:role/NonExistentRole"
+ "AWS": "arn:aws:iam::123456789012:role/my-actual-service-role"
},
"Action": "sts:AssumeRole"
}
]
}
For AWS service principals, the suffix must be the exact service DNS name:
{
"Principal": {
- "Service": "ec2.amazonaws.com.cn"
+ "Service": "ec2.amazonaws.com"
}
}
⚠️ China regions (
aws-cn) and GovCloud (aws-us-gov) use different partition strings (aws-cn,aws-us-gov). A trust policy copied from commercial AWS to GovCloud will always fail on principals.
Enterprise Best Practice — Least Privilege with Condition Keys
Never trust a bare role ARN without constraints. Bind the trust with sts:ExternalId for cross-account, or aws:PrincipalOrgID for org-wide access.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
- "AWS": "*"
+ "AWS": "arn:aws:iam::123456789012:role/ci-deploy-role"
},
"Action": "sts:AssumeRole",
+ "Condition": {
+ "StringEquals": {
+ "sts:ExternalId": "unique-secret-token-per-customer",
+ "aws:PrincipalOrgID": "o-xxxxxxxxxx"
+ },
+ "Bool": {
+ "aws:MultiFactorAuthPresent": "true"
+ }
+ }
}
]
}
For IRSA (EKS Pod Identity), the OIDC provider ARN must exactly match what's registered in IAM:
{
"Principal": {
- "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/WRONGCLUSTERID"
+ "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
+ "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:prod:my-service-account"
}
}
}
💡 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 — block invalid trust policies pre-merge:
# .checkov.yml
checks:
- CKV_AWS_61 # Ensure IAM role does not allow assume role from all principals
- CKV_AWS_274 # Ensure IAM policies do not allow credentials exposure
Run in pipeline:
checkov -d ./terraform --framework terraform --check CKV_AWS_61,CKV_AWS_274 --hard-fail-on HIGH
2. AWS IAM Access Analyzer — validate before apply:
# Validate a trust policy document before attaching it
aws iam-access-analyzer validate-policy \
--policy-document file://trust-policy.json \
--policy-type RESOURCE_POLICY
This returns FINDING with specific issueCode values like INVALID_ARN_RESOURCE before the policy ever touches a live role.
3. OPA/Conftest — enforce org-wide principal constraints:
# policy/iam_trust.rego
package aws.iam.trust
deny[msg] {
stmt := input.Statement[_]
stmt.Principal == "*"
not stmt.Condition
msg := "Trust policy wildcard principal requires a Condition block with PrincipalOrgID or ExternalId"
}
deny[msg] {
stmt := input.Statement[_]
principal := stmt.Principal.AWS
not startswith(principal, "arn:aws:iam::")
msg := sprintf("Malformed principal ARN: %v", [principal])
}
4. Terraform aws_iam_policy_document — use data sources, never raw JSON strings. The data source validates ARN structure at plan time and prevents the typos that cause this error in the first place.
data "aws_iam_policy_document" "trust" {
statement {
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${var.trusted_account_id}:role/${var.trusted_role_name}"]
}
actions = ["sts:AssumeRole"]
}
}