Terraform Error: S3 Backend 'AccessDenied' Reading or Writing State
Fix Terraform's S3 backend 'AccessDenied' error: diagnose missing IAM permissions, wrong bucket policy, KMS key access, bucket ownership, and region mismatches on the state object.
- #terraform
- #iac
- #troubleshooting
- #errors
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Exact Error Message
╷
│ Error: Failed to get existing workspaces: Unable to list objects in S3 bucket
│ "my-tf-state" with prefix "env:/": operation error S3: ListObjectsV2,
│ https response error StatusCode: 403, RequestID: 3B4C...,
│ HostID: ..., api error AccessDenied: Access Denied
╵
On write you may instead see:
Error: Failed to save state: failed to upload state: operation error S3: PutObject,
https response error StatusCode: 403, api error AccessDenied: Access Denied
What It Means
Terraform’s s3 backend stores your state file as an object in an S3 bucket. To read state it must ListBucket and GetObject; to write it must PutObject; and if the bucket enforces encryption it must be allowed to use the relevant KMS key. A 403 AccessDenied means AWS authenticated your credentials but the effective policy did not grant the specific S3 (or KMS) action Terraform attempted.
Access in S3 is the intersection of the IAM identity policy, the bucket policy, any KMS key policy, and Block Public Access / Object Ownership settings. A denial anywhere in that chain produces the same terse Access Denied. The message names the failing operation (ListObjectsV2, GetObject, PutObject), which tells you exactly which permission is missing.
Common Causes
- The IAM role/user lacks
s3:ListBucket,s3:GetObject, ors3:PutObjecton the state bucket or its objects. - The bucket enforces SSE-KMS and the identity lacks
kms:Encrypt/kms:Decrypt/kms:GenerateDataKeyon the key. - A restrictive bucket policy
Denys the principal, overriding the identity policy. - The credentials resolve to the wrong account, or an assumed role is not the one you expect.
- The
regionin the backend block does not match the bucket’s region, so the request hits the wrong endpoint. - Object Ownership / ACL settings block a cross-account writer from reading objects it did not own.
Diagnostic Commands
Confirm which identity Terraform is actually using:
aws sts get-caller-identity
Check the bucket’s region and reconcile it with your backend block:
aws s3api get-bucket-location --bucket my-tf-state
grep -A6 'backend "s3"' *.tf
Test the exact operations Terraform performs, with the same credentials:
aws s3api list-objects-v2 --bucket my-tf-state --prefix env:/ --max-items 1
aws s3api get-object --bucket my-tf-state --key path/to/terraform.tfstate /tmp/probe.tfstate
If encryption is involved, check the default encryption and the KMS key:
aws s3api get-bucket-encryption --bucket my-tf-state
Re-run init with debug logging to see the failing S3 call:
TF_LOG=DEBUG terraform init 2>&1 | grep -iE "AccessDenied|S3|kms"
Step-by-Step Resolution
- Verify the caller. If
aws sts get-caller-identityshows the wrong account or role, fix your profile/role assumption before touching policies:
export AWS_PROFILE=infra-admin
aws sts get-caller-identity
- Attach an IAM policy granting the minimum actions Terraform needs on the bucket and its objects:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::my-tf-state"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-tf-state/*"
}
]
}
- If the bucket uses SSE-KMS, also grant key usage on that key ARN:
{
"Effect": "Allow",
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "arn:aws:kms:us-east-1:111122223333:key/abcd-1234"
}
- Reconcile the region. The
regionin the backend must matchget-bucket-location:
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
}
}
- Check for an explicit
Denyin the bucket policy that overrides your grants, and remove or scope it:
aws s3api get-bucket-policy --bucket my-tf-state --query Policy --output text
- Re-initialize and confirm state operations succeed:
terraform init -reconfigure
terraform plan
For generating least-privilege IAM policies and backend blocks tailored to your bucket and KMS setup, the Terraform backend prompts in the prompt library can produce a ready-to-review policy document.
Prevention
- Grant a dedicated, least-privilege state-access policy rather than broad
s3:*, and keep it in code. - Keep the KMS key policy and the IAM identity policy in sync whenever you enable bucket encryption.
- Pin
regionin the backend block so a copied configuration cannot hit the wrong endpoint. - Use
bucket_key_enabledand a single documented state KMS key so access is easy to audit. - Avoid cross-account writes without
bucket-owner-full-control; prefer a shared role in the bucket’s account. - Test new roles with
aws s3apicalls before wiring them into CI so failures are caught early.
Related Errors
NoSuchBucket: The specified bucket does not exist— the bucket name or region is wrong, not a permission issue.ResourceNotFoundExceptionon lock — the DynamoDB lock table is missing, a separate backend problem.InvalidAccessKeyId/SignatureDoesNotMatch— bad or stale credentials rather than an authorization denial.Error acquiring the state lock— state locking contention, not S3 object access.
Frequently Asked Questions
Why does listing work but writing fail? ListBucket/GetObject and PutObject are separate actions; a read-only policy lets init succeed but apply fails on PutObject. Grant both on the correct resource ARNs.
Do I need s3:ListBucket on the bucket or the objects? ListBucket is a bucket-level action so its resource is the bucket ARN with no /*, while GetObject/PutObject are object-level and need /*. Mixing these up is the most common cause.
Could KMS be the real culprit? Yes — if default encryption uses SSE-KMS, S3 returns AccessDenied when your identity cannot use the key, even with full S3 permissions. Add the kms:*DataKey/Encrypt/Decrypt grants.
Why does one teammate work and another get denied? They assume different roles; run aws sts get-caller-identity for each and reconcile their policies. For more state-backend fixes, see the Terraform guides.
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.