Terraform "Error refreshing state: AccessDenied" reading the backend in CI
Before planning, Terraform reads the existing state from the backend. An S3 AccessDenied on GetObject means the runner role can reach the bucket but lacks read permission on the state key, so refresh fails before any resource is evaluated.
What this error means
plan fails early with "Error refreshing state" wrapping an S3 "AccessDenied" or "403" on GetObject for the state object path.
Error: Error refreshing state: operation error S3: GetObject,
https response error StatusCode: 403, ... api error AccessDenied: Access DeniedCommon causes
The runner role lacks s3:GetObject on the state key
The IAM policy grants the bucket but not the specific object path, so reading the state file is denied.
A bucket policy or KMS key blocks the role
A restrictive bucket policy, or a KMS key the role cannot use, denies the GetObject even when the IAM policy looks correct.
How to fix it
Grant read on the exact state path
- Confirm which role the runner assumes and the exact bucket/key from the error.
- Add s3:GetObject (and s3:ListBucket) for that prefix to the role policy.
- If the state is encrypted, grant kms:Decrypt on the key.
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-tf-state",
"arn:aws:s3:::my-tf-state/env/prod/*"
]
}Allow the KMS key for encrypted state
Add kms:Decrypt (and kms:GenerateDataKey for writes) on the key that encrypts the state object so refresh and apply can read and write it.
How to prevent it
- Scope the runner policy to the exact state bucket and key prefix.
- Include KMS decrypt/encrypt when the backend uses SSE-KMS.
- Verify the assumed role identity in CI before debugging policies.