Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 9 min read

Terraform Error: S3 Backend 'AccessDenied' Reading or Writing State

Quick answer

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
Free toolkit

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, or s3:PutObject on the state bucket or its objects.
  • The bucket enforces SSE-KMS and the identity lacks kms:Encrypt/kms:Decrypt/kms:GenerateDataKey on 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 region in 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

  1. Verify the caller. If aws sts get-caller-identity shows the wrong account or role, fix your profile/role assumption before touching policies:
export AWS_PROFILE=infra-admin
aws sts get-caller-identity
  1. 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/*"
    }
  ]
}
  1. 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"
}
  1. Reconcile the region. The region in the backend must match get-bucket-location:
terraform {
  backend "s3" {
    bucket = "my-tf-state"
    key    = "prod/network/terraform.tfstate"
    region = "us-east-1"
  }
}
  1. Check for an explicit Deny in 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
  1. 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 region in the backend block so a copied configuration cannot hit the wrong endpoint.
  • Use bucket_key_enabled and 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 s3api calls before wiring them into CI so failures are caught early.
  • NoSuchBucket: The specified bucket does not exist — the bucket name or region is wrong, not a permission issue.
  • ResourceNotFoundException on 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.

Free download · 368-page PDF

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.