skip to content

A security review is satisfied because the S3 bucket holding Terraform state has SSE-KMS enabled and the S3 backend block sets encrypt = true. Which threats does that actually close, and what remains wide open?

level: seniorimportance: should knowfreq 50%

answer

  1. encryption is below the API, not above it
  2. the service decrypts for any allowed caller
  3. two gates: bucket policy and key policy
  4. read access equals credential access
  5. old object versions keep old secrets

basics

~20 s

Server-side encryption protects state only against someone reading the storage underneath the API — stolen media, a mis-scoped bucket listing, an unauthorised backup copy. Any identity allowed to fetch the object gets it decrypted, so authorization, not encryption, is the real control.

solid answer

~50 s

Encryption at rest closes the below-the-API threats: physical media, replicated storage, an out-of-band copy of the bucket contents. It does nothing about the in-band path, because S3 decrypts transparently for any caller whose IAM policy permits `s3:GetObject` and whose principal is allowed by the KMS key policy. So the honest answer to "is the database password safe?" is: it is exactly as safe as the list of identities that can read that object. What makes SSE-KMS genuinely better than SSE-S3 here is the second gate — a customer-managed key with a key policy naming only the pipeline role and break-glass admins means bucket permissions alone are not sufficient, and every decryption is logged in CloudTrail. Pair it with S3 Block Public Access, a bucket policy that denies non-TLS requests, and a deliberate decision about versioning, since old object versions keep old secrets after rotation.

code

hcl · 9 lines
hcl
terraform {
  backend "s3" {
    bucket     = "acme-tfstate-prod"
    key        = "network/terraform.tfstate"
    region     = "eu-west-1"
    encrypt    = true
    kms_key_id = "arn:aws:kms:eu-west-1:111122223333:key/EXAMPLE"
  }
}

go deeper

for a junior

Know that state belongs in a private, encrypted bucket and never in Git, and that 'encrypted' does not mean 'unreadable by people with access'. Be able to name encryption at rest and access policy as two different things.

for a middle

Explain the mechanics: server-side encryption is applied by the storage service and decrypted transparently for authorised callers, so the object's confidentiality rests on the bucket policy and, with a customer-managed key, the key policy as well.

for a senior

Demonstrate threat modelling rather than checklist recital. Say which attacks encryption closes, name the in-band read path it does not, and design the surrounding controls: least-privilege policy, TLS-only condition, access logging, short-lived pipeline credentials, versioning with intent.

for a principal

Own the standard across the estate: who is entitled to read production state at all, how that entitlement is granted and reviewed, and how you push the organisation toward configurations where fewer live credentials pass through Terraform so the read-access question shrinks.

## What the two settings do The S3 backend's `encrypt = true` asks S3 to store the state object encrypted at rest; adding `kms_key_id` selects a specific KMS key instead of the bucket default. On the bucket side, default encryption can be configured to SSE-S3 (AWS-managed keys) or SSE-KMS (a KMS key, either the AWS-managed `aws/s3` key or a customer-managed key). Both settings are *server-side*: Terraform uploads the state over TLS, and S3 encrypts it as it lands. This is the crucial distinction to draw in an interview. The state object is encrypted **at rest in S3's storage layer**, not encrypted **as a document**. Terraform never holds a key, and the JSON it uploads and downloads is plain text on both ends. ## The threat model, honestly What encryption at rest actually closes: - physical media and decommissioned-disk exposure; - an attacker reading the storage substrate or replicated copies without going through the S3 API; - with a customer-managed KMS key: a caller who has been granted bucket permissions but is not permitted by the key policy — the key is a *second, independent gate*; - an audit requirement that says data at rest must be encrypted. What it does not close, and this is the whole point: - **any identity with `s3:GetObject` on that object.** S3 decrypts transparently. A read-only role handed out for "just looking at the bucket" is a credential-disclosure path; - a leaked or over-broad IAM role, a CI runner compromise, or a developer with `terraform state pull` rights; - copies that left the bucket: local `terraform.tfstate.backup`, saved plan files, CI artifacts, a `terraform show -json` dump pasted into a ticket; - old object versions, if versioning is enabled — after you rotate a leaked password, the previous versions still contain the old one until a lifecycle rule expires them. ## What a real control set looks like Encryption is one row in a table, not the table. 1. **Authorization first.** A dedicated state bucket, not a shared one. A bucket policy that names the small set of principals allowed to read and write the state prefix, with everything else denied. Nobody gets blanket `s3:*` on it because it happened to be convenient. 2. **A customer-managed KMS key with a restrictive key policy.** This is the reason to prefer SSE-KMS over SSE-S3: with the AWS-managed key, bucket permission is effectively sufficient. With a customer-managed key, the caller must also be permitted by the key policy, and every `Decrypt` call appears in CloudTrail with the caller identity — which gives you an audit trail of who read state. 3. **Block Public Access on the bucket**, and a policy condition denying requests where `aws:SecureTransport` is false. 4. **Versioning, with intent.** It is the standard recovery mechanism for a corrupted or truncated state file, so most teams want it — but recognise that it also retains every historical secret, and decide on a lifecycle expiry for noncurrent versions accordingly. 5. **Short-lived credentials.** A pipeline assuming a role via OIDC beats a static access key stored in the CI system, because the thing that reads state is then not a standing credential someone can exfiltrate. 6. **Server-side access logging or CloudTrail data events** on the bucket, so "who read production state" is an answerable question. ```hcl terraform { backend "s3" { bucket = "acme-tfstate-prod" key = "network/terraform.tfstate" region = "eu-west-1" encrypt = true kms_key_id = "arn:aws:kms:eu-west-1:111122223333:key/EXAMPLE" } } ``` ## The reframe that scores points The strongest version of this answer refuses the premise gently: encryption at rest answers a compliance question, not the question the reviewer thinks they asked. The right question is "who can read this object, and is that the same set of people we would trust with the production database password?" — because those two sets are, by construction, identical. If the answer is "the whole platform team plus three legacy roles nobody has audited", the encryption setting has not helped at all. And the deepest fix is upstream: the fewer live credentials that pass through Terraform in the first place — provider-managed passwords, write-only arguments, values fetched at apply time and never persisted — the less the read-access question matters.

  • Why prefer a customer-managed KMS key over the default S3-managed encryption for state?
    Because it adds an independent authorization gate. With SSE-S3, bucket permission is effectively enough to read the object. With a customer-managed key, the caller must also be allowed by the key policy, so a mis-scoped bucket grant alone does not disclose state — and every Decrypt call is attributed in CloudTrail, giving you an audit trail of state reads.
  • Bucket versioning is on. How does that interact with rotating a password that leaked through state?
    Versioning is the standard recovery path for a corrupted state object, so keep it — but understand that every noncurrent version still holds the pre-rotation secret. Rotation invalidates the credential, which is what matters; the stale copies are only a problem if the old value stays valid. Set a lifecycle rule expiring noncurrent versions so the retention window is deliberate.
  • Your CI pipeline needs to read and write this state. What is the safest way to give it access?
    A role assumed through OIDC federation from the CI provider rather than a static access key stored as a pipeline secret. The credential is then short-lived, scoped by a trust policy to the specific repository and branch, and there is no long-lived key sitting in the CI system for someone to exfiltrate.

saying these in an interview costs you the question

  • Says encryption at rest means readers cannot see the secret
  • Treats SSE-S3 and a customer-managed KMS key as equivalent controls
  • Forgets that prior object versions retain rotated secrets
  • Grants broad bucket read access because state is 'just metadata'
  • Believes encrypt = true encrypts state client-side before upload

context