| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.16.1hashicorp/aws 6.63.0awscli 2.36.38 |
| Applies to | Any host with Terraform and the AWS provider; examples use ap-south-1 |
| Privilege | AWS identity that can call sts:GetCallerIdentity and sts:DecodeAuthorizationMessage; normal user on the workstation |
| Scope | Diagnose UnauthorizedOperation and AccessDenied from the HashiCorp AWS provider during terraform plan or apply — confirm caller identity, decode the authorization failure message, read the denied API action and resource, and fix IAM permissions. Does not cover creating IAM users, organization SCP authoring, or replacing the AWS credentials error when no identity is found at all. |
| Related guides | Terraform providers AWS CLI tutorial |
You ran terraform apply, Terraform reached AWS, and the provider returned a 403-style failure:
Error: creating EC2 Instance: operation error EC2: RunInstances, …
api error UnauthorizedOperation: You are not authorized to perform this operation.
… Encoded authorization failure message: wXYQTDpQds6-FfUhMJvMDEqHX1PcU9DPX_xJFxPDi1n…That is different from missing credentials. AWS recognized your IAM identity and refused one API call.
What UnauthorizedOperation means
- Your user or role can authenticate, but the IAM policy does not allow the action Terraform just called
- The error may name the action directly (
ec2:RunInstances,s3:CreateBucket) or hide details behind an encoded blob - Fixing it is an IAM permission change for that identity — not a Terraform syntax problem
No valid credential sources found instead — see Fix Terraform AWS credentials error. This article assumes AWS already knows who you are.
Confirm which identity Terraform uses
Before you change IAM policies, verify which principal signed the failed request. Terraform uses the same credential chain as the AWS CLI in that shell.
aws sts get-caller-identitySample output:
{
"UserId": "AIDAU7QAIUYJBSXAT2W5",
"Account": "342524208658",
"Arn": "arn:aws:iam::342524208658:user/deploy-user"
}The Arn line is the identity you need to fix. If you use a named profile, run the same check with that profile active:
aws sts get-caller-identity --profile productionWhen AWS_PROFILE or a profile argument in the provider block points at the wrong profile, you may be debugging permissions for user A while Terraform runs as user B.
Two shapes of permission errors
Not every 403 includes an encoded message.
Plain AccessDenied
AWS prints the action and resource directly:
api error AccessDenied: User: arn:aws:iam::342524208658:user/deploy-user is not
authorized to perform: s3:CreateBucket on resource: "arn:aws:s3:::my-tf-bucket"
because no identity-based policy allows the s3:CreateBucket actionRead the perform: clause and add that action to the identity policy (or ask your admin to).
UnauthorizedOperation with encoded message
Common on EC2 and some other services. AWS hides the details behind a long blob. Copy the string after Encoded authorization failure message: from your Terraform output. A failed terraform apply on an aws_instance prints something like this:
Error: creating EC2 Instance: operation error EC2: RunInstances, …
api error UnauthorizedOperation: You are not authorized to perform this operation.
User: arn:aws:iam::342524208658:user/deploy-user is not authorized to perform: ec2:RunInstances …
Encoded authorization failure message: wXYQTDpQds6-FfUhMJvMDEqHX1PcU9DPX_xJFxPDi1n…Even when the text already names ec2:RunInstances, decode the blob — it confirms the principal, resource ARN, and whether a Service Control Policy or permission boundary blocked the call. Terraform does not decode this for you automatically.
Decode the authorization failure message
Copy the encoded string from your Terraform error into a shell variable so you do not truncate it by mistake:
ENC='wXYQTDpQds6-FfUhMJvMDEqHX1PcU9DPX_xJFxPDi1nL1JH4zv3t6q-6OsKBuJCJ9lB_EKCV6HLWmAMiq44G1C4mslFq4lGl0jy9Jr8u-xDADn84pYjw-G3XsiNHrp9cqPDGaBZRDqmVeUfKTWptBoCirn1FaxpjhpLaohZxJt2GVsQQtpF0zAW0LbRdYzmXUYf7gWjweMObo6kKoTCLuONaVfq26zOtOTiwTAf8tY29pgqmXTzNwcZG_My3nw9nVRi_zc0fEqQCp7dXmpW681aGGJreAhIt5O655Gg6l2ROfTmuss8JsAn4jyYxtR9xVu7SxVDSDPTg3iebk2B56wVLohiG4cfeWDCU7XJOqdYnRHuYgV4W9syo_t6CoQdx5Ev6iOzLqzs2eqTjNfAzhPMp7sRkBArnRAhIsA5SzsA4_YWejYTnpAAlSpTIbyZ4SNJd845GuvEMKi1FZOO8_1dAW-8yHjmydHkylLlwrjQzgVrgV_Q8EIDBTRAQ63H821h9eQyV805ryJUJdRTb6F9h1JcJhZ7P3I3OD4l8KSzMK1QUTEiyv-36akIvm__D9Ln1QAsVKl2XETMmaqcKgjKgk1WzeTRcAIHwIQaJojbaGmUVquSw5DDjM9R0f597ERjEpd7KtavDeTP2EeSXMslml1I3MTMyJkz4KVfzJ2PdCS4iOA9gmIjiPCqlRtJjols6YW0AImVniEz5wn_gZTWHp_n2VrLqAbtpH5ZaHWw8fHg3Erk-SuxxqL016SJ2qo2SOQXms_Wuw0nL8wj_QyarpWK2j2mmRdIZSVil48P59xMG9t5nxXj2ekis'Decode with the AWS CLI using the same credentials that received the error:
aws sts decode-authorization-message --encoded-message "$ENC" --output json | jq -r '.DecodedMessage' | jq .Sample output (trimmed to the fields you act on):
{
"allowed": false,
"explicitDeny": false,
"context": {
"principal": {
"arn": "arn:aws:iam::342524208658:user/deploy-user"
},
"action": "RunInstances",
"resource": "arn:aws:ec2:ap-south-1:342524208658:instance/*"
}
}How to read the JSON:
context.action— IAM API name to allow (ec2:RunInstancesmaps to theRunInstancesEC2 API)context.resource— ARN pattern Terraform targetedcontext.principal.arn— confirms which identity was deniedexplicitDeny: true— look for an SCP, permission boundary, or explicitDenystatement instead of adding onlyAllowallowed: falsewith emptyfailures— still useful; theactionandresourcefields tell you what to request
Your identity also needs sts:DecodeAuthorizationMessage to run this command. If decode itself fails with AccessDenied, ask an administrator to decode the blob or grant that action on *.
Fix the missing IAM permission
You now know the API action Terraform needs. The fix depends on your role in the account.
If you manage IAM policies yourself, attach an Allow for the decoded action on the right resource scope. For ec2:RunInstances:
{
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:DescribeInstances",
"ec2:CreateTags",
"ec2:TerminateInstances"
],
"Resource": "*"
}Terraform often needs related Describe* calls during plan and refresh, not only the create action. Start with the decoded action, then add describe permissions if the next error names them.
If you are on a team, open a ticket with:
- The Terraform resource address (for example
aws_instance.web) - The decoded
actionandresource - The principal ARN from
get-caller-identity - The AWS region from your provider block
If explicitDeny is true, a broader Allow in your user policy will not help until someone removes or narrows the deny — often an organization SCP or permissions boundary.
After the policy change, wait a few seconds for IAM propagation, then re-run Terraform:
terraform planWhen the plan is clean, apply again and confirm at the prompt:
terraform applyType yes only after the destroy/create list looks correct.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
UnauthorizedOperation with encoded message |
Missing Allow for the API action in the identity policy |
Decode message; add action on the resource scope |
AccessDenied with explicit perform: clause |
Same as above, but AWS already printed the action | Add the named action; decode optional |
DecodeAuthorizationMessage AccessDenied |
Your identity cannot call STS decode | Ask admin to decode, or grant sts:DecodeAuthorizationMessage |
InvalidAuthorizationMessage on decode |
Wrong blob copied, or different credentials than the failed call | Re-copy from Terraform output; export the same profile or keys |
| Plan works, apply fails on a different action | Plan does not call every API that apply uses | Decode the apply error; add the second action (common with iam:PassRole) |
Fixed RunInstances, now DescribeTags fails |
Provider refresh needs extra describe permissions | Add the newly named ec2:Describe* action |
| Same policy works in console, not in Terraform | Different identity or region | Compare aws sts get-caller-identity with the decoded principal.arn |
For broader Terraform failure stages, see Terraform troubleshooting.
Summary
UnauthorizedOperation during terraform plan or apply means AWS authenticated your identity but denied a specific API call. That is an IAM permissions problem, not missing credentials and not a bug in your .tf syntax.
Start by confirming which principal Terraform uses with aws sts get-caller-identity. When the error includes Encoded authorization failure message, copy the full string and run aws sts decode-authorization-message with the same credentials. The decoded JSON names the action, resource, and principal so you can request the right Allow instead of guessing.
Some errors already state the missing action (s3:CreateBucket, iam:PassRole) without an encoded blob — treat those the same way and add the named permission. After IAM propagation, run terraform plan and terraform apply again and read the plan before you confirm. If decode shows explicitDeny: true, look for organization SCPs or permission boundaries, not only user policy edits.
References
- AWS STS decode-authorization-message
- AWS re:Post — Decode EC2 launch authorization errors
- IAM policy reference — Actions, resources, and condition keys
- HashiCorp Terraform AWS provider
- HashiCorp Terraform — terraform apply
Frequently Asked Questions
Is UnauthorizedOperation the same as No valid credential sources found?
No. No valid credential sources found means Terraform could not sign requests at all. UnauthorizedOperation means AWS knew your identity and refused one action.
Do I need decode when the error already names ec2:RunInstances?
Decoding is still useful. It confirms the principal and resource ARN and shows whether an explicit deny blocked the call.
Can I decode with my personal admin user if CI failed?
Only if you use the same credentials that received the encoded error. Otherwise AWS returns InvalidAuthorizationMessage.
Why did plan succeed but apply fail?
Plan often skips create APIs. Apply calls them. The permission gap appears at apply time.
Does adding AdministratorAccess fix this?
It would, but it violates least privilege. Add only the actions Terraform needs for your module.

