How to Fix Terraform Permission Errors in AWS

When Terraform fails with an AWS permission error, it is always an IAM authorization failure. But Terraform adds failure points that generic AWS guides don’t cover: backend and state access, tagging calls, iam:PassRole, assume-role flows, and partially applied resources. Fixing it quickly means knowing which layer failed, not just which action was denied.

This page gives you a fast checklist, a breakdown of each failure pattern, and the specific fix for each one.

Simple explanation

Think of Terraform like a general contractor building a house. Getting a building permit doesn’t mean you can do the electrical work. Each trade needs its own permit. If the electrician shows up without one, the job stops at that step, even though the foundation is already poured.

Terraform works the same way. A single terraform apply makes dozens of AWS API calls across multiple services:

  • sts:AssumeRole to authenticate
  • s3:GetObject and s3:PutObject to read and write state
  • ec2:CreateVpc to create the resource
  • ec2:CreateTags to apply tags
  • ec2:DescribeVpcs to read the resource back into state

Each call needs its own IAM permission. A single missing permission breaks the step it covers, even if every earlier step succeeded. That is why you can see a resource created in the AWS console but Terraform still reports an error: creation worked, tagging failed.

Quick diagnosis checklist

Run through this before debugging the policy:

  1. Who is Terraform authenticating as? Run aws sts get-caller-identity in the same terminal Terraform runs in.
  2. Did the error happen during terraform init or during plan/apply? Init failures are almost always backend or state access. Apply failures are resource or IAM action gaps.
  3. What is the exact denied action, resource, and principal? Read the error message carefully. These three values tell you exactly what to fix.
  4. Is this a tagging failure? If the resource was created but the error mentions tagging, add the missing tag action.
  5. Is iam:PassRole in the error? This is a specific pattern when Terraform attaches a role to Lambda, ECS, EC2, or a similar resource.
  6. Are SCPs, permissions boundaries, or session policies involved? An explicit deny from a Service Control Policy overrides any allow in the identity policy.
  7. Is this a trust policy problem? A role’s trust policy controls who can assume it. A role’s permission policy controls what the assumed role can do. These are separate.
  8. Did a previous apply fail partway? Run terraform plan before re-running terraform apply to see what Terraform thinks needs to happen next.

How Terraform permission errors work in AWS

AWS enforces two concepts that beginners often mix up:

  • Authentication: proving who you are (access keys, assumed role, instance profile)
  • Authorization: whether the authenticated identity is allowed to perform the requested action

A good analogy: authentication is your ID card that gets you into the building. Authorization is the key card that unlocks specific rooms. You can be a real employee (authenticated) but still be blocked from the server room (not authorized).

Terraform permission errors are always authorization failures. But they can happen at any point in the Terraform workflow, and the root cause depends on where they occur.

There are two distinct categories:

Backend and state access errors

These happen during terraform init or at the start of terraform plan/apply when Terraform reads or writes the remote state file.

For an S3 backend, Terraform needs:

  • s3:ListBucket to list workspaces and check the bucket
  • s3:GetObject to read the current state
  • s3:PutObject to write updated state

For state locking with S3 native locking (use_lockfile = true), Terraform stores a .tflock file in S3 and also needs s3:DeleteObject to release the lock. DynamoDB-based locking is deprecated in newer Terraform versions, but older setups may still use it. Those need dynamodb:DescribeTable, dynamodb:GetItem, dynamodb:PutItem, and dynamodb:DeleteItem on the lock table.

See Terraform State Management for full backend configuration details.

Resource and apply errors

These happen during terraform plan or terraform apply when Terraform calls the AWS APIs for your actual infrastructure. Each resource lifecycle step (create, tag, read-back, update, destroy) may require a different IAM action. Missing any one of them causes Terraform to fail at that step.

Step 1: Confirm which AWS identity Terraform is using

This is the single most common source of confusion in Terraform permission errors.

aws sts get-caller-identity

The output shows Account, UserId, and Arn. Check all three:

  • Account: is this the right AWS account? Not a dev account when deploying to prod?
  • ARN: is this the IAM user or role intended for Terraform?
  • Role/user: does this identity have the Terraform IAM policy attached?

Terraform uses the AWS credential chain in this order:

  1. Environment variables: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
  2. AWS_PROFILE environment variable
  3. Default profile in ~/.aws/credentials
  4. EC2 instance metadata or ECS task role (when running on AWS)

If you have multiple profiles, Terraform uses the default profile unless overridden. Check the provider block:

provider "aws" {
  region  = "us-east-1"
  profile = "terraform-deployer"
}

For assume-role workflows, Terraform can assume a role directly in the provider block:

provider "aws" {
  region = "us-east-1"
  assume_role {
    role_arn = "arn:aws:iam::111122223333:role/terraform-deployer"
  }
}

When this is configured, the trust policy of terraform-deployer must allow the calling identity to assume it. If the trust policy is wrong, Terraform fails here with an sts:AssumeRole error before doing anything else.

STS temporary credentials and role assumption are the recommended patterns for CI/CD and short-lived Terraform access.

Before every production apply: Run aws sts get-caller-identity and confirm the account ID and role ARN match what you expect. A leftover AWS_PROFILE from an earlier terminal session can silently point Terraform at the wrong account.

Step 2: Classify the error — backend, state, or resource

Error patterns differ by layer:

S3 backend access denied

Error: Failed to get existing workspaces: AccessDenied: Access Denied
  status code: 403

Happens during terraform init or at the start of a plan/apply. The Terraform identity is missing S3 permissions on the state bucket.

State lock failure

Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional request failed

A previous Terraform process holds the lock, or crashed without releasing it. Use terraform force-unlock LOCK_ID to release it. Only do this when you are certain no other process is actively running.

Resource creation denied

Error: error creating S3 Bucket (my-app-bucket): AccessDenied:
  status code: 403
  with aws_s3_bucket.app_bucket, on main.tf line 12

The Terraform identity is missing the IAM action for that resource creation step.

Tagging denied

Error: error tagging resource (arn:aws:ec2:us-east-1:111122223333:vpc/vpc-0abc123):
  AccessDenied: You are not authorized to perform this operation.

The resource was created. The tagging step failed. The policy has the create action but not the tag action.

AssumeRole denied

Error: configuring Terraform AWS Provider:
  AssumeRole: failed to assume role
  AccessDenied: User is not authorized to assume role

Either the trust policy of the target role does not allow the calling identity, or the calling identity is missing sts:AssumeRole permission.

Step 3: Extract the exact denied action and resource

Every Terraform permission error gives you enough to fix it. Pull out:

FieldWhere to find it
Principalaws sts get-caller-identity
ActionThe error message (“error creating” translates to s3:CreateBucket)
ResourceThe ARN or name in the error
PhaseWas it init, plan, apply, tagging, or destroy?
Terraform resourceThe with aws_xxx.name line in the error
File and lineThe on main.tf line N in the error

If the error message doesn’t show the exact IAM action, use CloudTrail to look up which API call was denied:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=terraform-deployer \
  --start-time 2026-05-13T10:00:00Z \
  --end-time 2026-05-13T11:00:00Z \
  --query 'Events[*].[EventName,ErrorCode]' \
  --output table

Building a minimal policy? Run Terraform against a dev account with CloudTrail enabled, then query every API call it made. This gives you the exact list of actions to include in the policy instead of guessing.

Step 4: Common Terraform-specific permission failure patterns

Missing tagging permissions

The issue: Tags are applied after resource creation as a separate API call. A policy can allow resource creation but block tagging.

How it appears: Terraform creates the resource but immediately fails on a *:CreateTags or *:PutBucketTagging error. The resource now exists in AWS but is partially configured.

Fix: Add the tagging action to the policy. Common ones:

{
  "Effect": "Allow",
  "Action": [
    "ec2:CreateTags",
    "s3:PutBucketTagging",
    "lambda:TagResource",
    "ecs:TagResource",
    "rds:AddTagsToResource"
  ],
  "Resource": "*"
}

After a partial apply: Do not immediately re-run terraform apply. Run terraform plan first and review it carefully. Terraform may try to recreate, destroy, or modify a resource that already exists in AWS but is only partially recorded in state.

Missing iam:PassRole

The issue: When Terraform creates a resource that attaches an IAM role (Lambda execution role, ECS task role, EC2 instance profile), the Terraform identity must have iam:PassRole on that role’s ARN. AWS requires this to prevent privilege escalation. Without it, anyone who could create a Lambda could silently attach an admin role to it.

How it appears:

Error: error creating Lambda Function (my-function): AccessDenied:
  User is not authorized to perform: iam:PassRole on resource:
  arn:aws:iam::111122223333:role/my-lambda-execution-role

Fix: Add iam:PassRole scoped to the exact role ARN, not *:

{
  "Effect": "Allow",
  "Action": "iam:PassRole",
  "Resource": "arn:aws:iam::111122223333:role/my-lambda-execution-role"
}

This applies to Lambda, ECS task definitions, EC2 instance profiles, CodeBuild, CodePipeline, and any AWS service that assumes an IAM role at runtime. See Managing IAM with Terraform for how to structure these policies cleanly.

Wrong AWS profile or stale session credentials

The issue: AWS_PROFILE set from a previous terminal session causes Terraform to authenticate as the wrong identity silently. Stale session tokens cause failures mid-apply.

How it appears: The policy looks correct but Terraform still gets denied. aws sts get-caller-identity shows an unexpected account or role.

Fix:

# Check what Terraform will use
printenv | grep AWS
aws sts get-caller-identity

# Override explicitly
AWS_PROFILE=terraform-prod terraform apply

Common gotcha: If you recently ran aws sso login or assumed a role in another terminal, that session may be set as the active profile. Terraform inherits this silently. Always verify the identity before a production apply.

Role trust policy problems

The issue: Think of it like a nightclub. The trust policy is the bouncer at the door. The permission policy is the list of things you’re allowed to do once you’re inside. A role can have all the permissions in the world, but if the trust policy doesn’t list you as a Principal, the bouncer won’t let you in.

How it appears: sts:AssumeRole denied, even though the Terraform identity has sts:AssumeRole in its own policy.

Fix: Check the trust policy of the target role:

{
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam::111122223333:user/terraform-user"
    },
    "Action": "sts:AssumeRole"
  }]
}

The Principal must exactly match the identity running Terraform. See IAM Roles Explained for the full breakdown of trust policy structure.

SCP, permissions boundary, or session policy restrictions

The issue: IAM policy evaluation has multiple layers, each of which can produce an explicit deny. An allow in the identity policy means nothing if a Service Control Policy at the organization level, a permissions boundary on the role, or a session policy restricts the action.

How it appears: The identity policy looks correct and complete. Terraform still gets denied. CloudTrail shows explicitDeny in the event.

Explicit deny wins, always. A Deny in any policy layer overrides any Allow, anywhere. If CloudTrail shows explicitDeny, find the source of the Deny before adding more Allow statements. More Allows won’t help until the Deny is resolved.

Fix: Check each layer:

  1. Does an SCP at the OU or account level deny the action?
  2. Does the role have a permissions boundary that excludes the action?
  3. If the role was assumed with --policy, is the session policy too narrow?

Use the IAM Policy Simulator to evaluate the full chain for a given principal, action, and resource.

Bucket policy or VPC endpoint policy restrictions

The issue: Resource-based policies can deny access independently of identity policies. A bucket policy on the Terraform state bucket, or a VPC endpoint policy that restricts which principals can reach the endpoint, can block Terraform even when the identity policy allows the action.

How it appears: Terraform fails to access the state bucket or a resource even though the identity policy looks correct.

Fix: Check the S3 bucket policy on the state bucket. If the Terraform runner is inside a VPC, check whether a VPC endpoint policy is restricting access. See IAM Access Denied Errors for the full policy evaluation sequence.

Remote state bucket or lock permissions

The issue: The Terraform identity has permissions to manage resources but not to read or write the S3 state file. This causes Terraform to fail before it even reads your configuration.

Required permissions on the state bucket:

{
  "Effect": "Allow",
  "Action": [
    "s3:ListBucket",
    "s3:GetObject",
    "s3:PutObject"
  ],
  "Resource": [
    "arn:aws:s3:::my-terraform-state-bucket",
    "arn:aws:s3:::my-terraform-state-bucket/*"
  ]
}

For S3 native locking (use_lockfile = true), the .tflock file also needs s3:DeleteObject to release the lock:

{
  "Effect": "Allow",
  "Action": "s3:DeleteObject",
  "Resource": "arn:aws:s3:::my-terraform-state-bucket/*.tflock"
}

For older setups using DynamoDB locking, add:

{
  "Effect": "Allow",
  "Action": [
    "dynamodb:DescribeTable",
    "dynamodb:GetItem",
    "dynamodb:PutItem",
    "dynamodb:DeleteItem"
  ],
  "Resource": "arn:aws:dynamodb:us-east-1:111122223333:table/terraform-state-lock"
}

Partially applied resources after a failed run

The issue: When Terraform fails mid-apply, some resources may have been created but not fully configured. Terraform records what it knows in state, but the state may be incomplete or out of sync with what actually exists in AWS.

How it appears: The next terraform apply tries to modify, destroy, or recreate resources in unexpected ways.

Fix: Always run terraform plan after a failed apply before running terraform apply again. Review the plan carefully. If a resource exists in AWS but not in Terraform state, use terraform import to bring it under management before re-running apply.

How it works

Think of a Terraform run like following a recipe. You can’t skip to step 6 (serve the food) if step 3 (preheat the oven) failed. Each step depends on the previous one completing successfully, and each step needs its own set of “ingredients” (IAM permissions).

A full Terraform run touches AWS in this sequence:

  1. Authenticate: resolve credentials from the environment, profile, or provider block; optionally assume a role via sts:AssumeRole
  2. Read backend/state: contact the S3 bucket (and lock table or lockfile if configured) to get the current state file
  3. Read current infrastructure: call Describe* and List* APIs to refresh the real-world state of existing resources
  4. Plan changes: compare desired state (your .tf files) to actual state and produce a diff
  5. Create/update resources: call create, update, or delete APIs for each changed resource
  6. Tag and configure resources: apply tags, policies, and secondary configuration as separate API calls
  7. Write updated state: write the new state back to S3

A permission error at any step stops the run at that point. Steps 1 through 4 are needed for plan. Steps 1 through 7 are needed for apply. Step 2 is needed for init.

This is why the same Terraform configuration can fail with completely different errors on different runs. The error depends on which step was reached and which action was missing.

When to use this

This page covers:

  • AccessDenied errors during terraform init, plan, or apply on AWS
  • iam:PassRole denials when Terraform creates Lambda, ECS, EC2, or similar resources
  • sts:AssumeRole failures in Terraform provider blocks
  • S3 backend state or lockfile access failures
  • Tagging failures after successful resource creation
  • Wrong profile or wrong account problems
  • Partial apply failures and state drift

Use IAM Access Denied Errors instead if:

  • The access denied error is not from Terraform (a direct CLI call or an application runtime)
  • You need the full IAM policy evaluation sequence or policy simulator walkthrough
  • The error is unrelated to Terraform’s workflow

Terraform permission errors vs generic AWS AccessDenied errors

DimensionTerraform on AWSGeneric IAM AccessDenied
Backend/stateSeparate permission set for S3 and lockNot applicable
TaggingSeparate API call; can fail after create succeedsSame pattern, less common to encounter
Partial apply riskResource created, state drift possibleNo partial state risk
Profile/credential confusionProvider block can silently override env varsOnly env vars and profile chain
iam:PassRoleRequired for Lambda, ECS, EC2, and other role-attaching resourcesNot Terraform-specific, but applies equally
Trust policy vs permission policyBoth matter; assume-role failures are commonTrust policy only matters for cross-account calls
Layer of denialSCP, boundary, session policy, bucket policy, VPC endpoint policy, or identity policySame layers, but Terraform exercises more of them in a single run

Common mistakes

  1. Using personal admin credentials for Terraform: running Terraform as an account admin bypasses least-privilege and makes auditing difficult. Use a dedicated deployment role. See IAM Roles Explained for how to set one up.
  2. Forgetting iam:PassRole: every resource that attaches an IAM role at runtime requires iam:PassRole in the Terraform identity policy, scoped to that role’s ARN. Applies to Lambda, ECS task definitions, EC2 instance profiles, CodeBuild, and more.
  3. Mixing backend and provider assumptions: the identity that reads the state bucket and the identity that creates resources can be different. Make sure both have the right permissions.
  4. Silently using the wrong profile: AWS_PROFILE left over from a previous terminal session causes Terraform to authenticate as the wrong identity without warning. Run aws sts get-caller-identity before every production apply.
  5. Assuming one allow policy is enough when SCPs or boundaries exist: an explicit deny at the organization level from a Service Control Policy overrides any allow in the identity policy. Both layers must allow the action.
  6. Confusing trust policy errors with permission errors: sts:AssumeRole denied usually means the trust policy of the target role does not list the calling identity as a Principal. Check the trust policy, not the permission policy.
  7. Re-running apply after partial failure without checking state: always run terraform plan after a failed apply before running terraform apply again. Skipping this can cause Terraform to create duplicate resources or apply conflicting changes.

Frequently asked questions

Why does terraform init fail for one reason but terraform apply fail for a different one?

They touch different AWS services. terraform init reads the backend (usually S3, plus a lockfile or DynamoDB table), so init failures are almost always backend access problems. terraform apply calls the AWS APIs for each resource in your config: Lambda, EC2, IAM, S3, and so on. The permissions needed for init and apply are completely separate, and a role that can access the state bucket may still be missing actions needed to create or tag the actual resources.

Why can Terraform create a resource but then fail when tagging it?

Creating a resource and tagging it are separate IAM actions. s3:CreateBucket creates the bucket, but s3:PutBucketTagging applies tags. If the policy allows CreateBucket but not PutBucketTagging, the bucket is created and the tag step immediately fails. This leaves a partially-configured resource in AWS and a partially-updated state file. Always run terraform plan after a failed apply before re-running terraform apply.

Do I need admin permissions to run Terraform in AWS?

No. Terraform only needs the permissions required for the specific resources it manages, but calculating that exact set is tedious. A practical approach is to start with the obvious create/describe/delete actions for each resource type, run terraform apply, and add missing permissions as Terraform reports errors. Use CloudTrail to audit exactly which API calls Terraform made during a run.

How do I fix iam:PassRole errors in Terraform?

Add iam:PassRole to the Terraform identity policy scoped to the exact execution role ARN, not *. Example: {"Effect": "Allow", "Action": "iam:PassRole", "Resource": "arn:aws:iam::111122223333:role/my-lambda-execution-role"}. This error appears when Terraform creates a Lambda function, ECS task definition, EC2 instance profile, or any other resource that attaches an IAM role.

Why does the AWS CLI work fine but Terraform still gets AccessDenied?

Terraform and the AWS CLI may be using different credentials. The CLI uses AWS_PROFILE or the default profile. Terraform uses whatever credentials are configured in the provider block, or environment variables, or the default profile. Run aws sts get-caller-identity in the same environment Terraform runs in to confirm they match. Also check whether the Terraform provider block has an explicit profile or assume_role block that overrides the environment.

Last verified: 13 May 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.