Skip to main content
RunBook Academy

TerraformXI · State Security and LifecycleProduction Terraform

State Security Threats

Foundation⏱ ~12 minbash

What you'll learn

  • Identify the sensitive data that ends up in state (resource IDs, ARNs, secrets passed through)
  • Define the threat model: who can read state, who can write state, what they can do
  • Recognise the per-environment boundary and why shared state is a high-risk pattern
  • Apply the principle of least privilege to state backend access

Prerequisites

None — start here.

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

A state file for a moderately sized production estate is a 100–500 KB JSON document. It contains every resource ID, every ARN, every IP address, every output value, every tag. If a database password was ever passed through Terraform — even as a variable — it is in there, in plain text. Anyone who can read the state file can see the entire topology of the production infrastructure. Anyone who can write the state file can declare that anything exists. The threat model for state is the threat model for the entire estate.

What is in state that an attacker wants

Three categories of data, in increasing severity:

1. Resource IDs and ARNs. Every resource has an id attribute that holds the cloud identifier. EC2 instance IDs, RDS ARNs, S3 bucket names, IAM role ARNs. An attacker who reads state can map the entire production estate without ever calling the cloud API.

2. Network and configuration metadata. CIDR blocks, subnet IDs, security group rules, IAM policy documents, KMS key ARNs. The full network topology is in state. An attacker can plan lateral movement without any recon.

3. Secrets that flowed through Terraform. This is the bad one. A random_password resource, a database password passed through a data source, a TLS private key, an API token. The secret is in state as a plain-text string. A read of the state file is a read of the secret.

Threat model for the state file:

   Read access  →  full topology, every ID, every secret in plain text
   Write access →  declare any infrastructure; lock out the team
   Delete access →  unmanaged infrastructure, recovery from backup

Who can read and write state in production

In a typical AWS setup with S3 + DynamoDB:

RoleS3 readS3 writeDynamoDB access
Apply IAM roleYesYesYes
Plan IAM roleYesNoYes
Read-only auditorYesNoNo
Developer (read)NoNoNo
Developer (apply)Yes (via CI)Yes (via CI)Yes (via CI)

The principle: the smallest number of IAM principals have write access. Read access is broader but still controlled. No developer has direct state access; every apply runs through a CI pipeline that holds the IAM role.

For Terraform Cloud, the equivalent:

RoleWorkspace readWorkspace writeVariable access
Workspace adminYesYesYes
Developer (apply)Yes (via run)No (Terraform runs)No
Read-only auditorYesNoNo
Variable viewerNoNoYes

Terraform Cloud adds a layer: developers do not have IAM to the backend; they trigger runs through the API. The audit log is the Terraform Cloud run history.

The per-environment boundary

Production, staging, and development should have separate state backends, not separate prefixes in a shared bucket.

production:
   S3 bucket:    tfstate-production
   DynamoDB:     tfstate-locks-production
   KMS key:      kms-prod
   IAM role:     terraform-apply-production

staging:
   S3 bucket:    tfstate-staging
   DynamoDB:     tfstate-locks-staging
   KMS key:      kms-staging
   IAM role:     terraform-apply-staging

development:
   S3 bucket:    tfstate-dev
   DynamoDB:     tfstate-locks-dev
   IAM key:      kms-dev
   IAM role:     terraform-apply-dev

Three independent backends. The blast radius of a compromise in development is limited to development. A compromised staging credential cannot read or write production state. The separation is enforced by AWS account boundaries, IAM policies, and KMS key policies.

The threat categories

Four threats to model, in increasing severity:

1. Read by an authorised insider. A developer with read access to production state for debugging purposes sees the entire production topology. This is acceptable if the developer is authorised and the access is logged. The risk is the developer who is authorised but not careful: printing state to a log, copying it to a laptop, sharing it in a Slack DM.

2. Read by an unauthorised party. A misconfigured S3 bucket policy, a leaked IAM credential, a stolen developer laptop. This is a security incident. The response is in the incident lesson.

3. Write by an unauthorised party. A misconfigured IAM role that allows s3:PutObject to the state bucket. An attacker can replace the state with one that declares resources they control. This is a full takeover of the production topology from Terraform’s perspective.

4. State corruption or deletion. An attacker (or an operator under pressure) deletes the state. Recovery is from the versioned backup, but the recovery window depends on the backup cadence and the RPO.

The principle of least privilege, applied to state

For each IAM principal that touches state, the policy should be the minimum that allows the work:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::tfstate-production/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:DeleteItem"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/tfstate-locks-production"
    },
    {
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:GenerateDataKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/<key-id>"
    }
  ]
}

Three grants: S3 object access to the state bucket only, DynamoDB access to the lock table only, KMS access to the state encryption key only. No wildcard resources. No s3:ListBucket (unless the team genuinely needs to enumerate objects).

The principle extends to the human side:

  • Developers do not hold the apply IAM role. CI holds it.
  • Developers trigger runs through CI, which assumes the role.
  • The CI logs the run, the operator, and the diff.
  • The state IAM role is rotated on the same cadence as any other privileged credential.

Validation

READ-ONLY

# Confirm the backend bucket is what you expect
terraform state pull > /tmp/state.json
echo "Bucket: $(grep -oE 'bucket = "[^"]+"' /tmp/state.json || echo 'no bucket field')"

# Confirm encryption-at-rest is on
aws s3api get-bucket-encryption --bucket tfstate-production

# Confirm versioning is on
aws s3api get-bucket-versioning --bucket tfstate-production

# Confirm the IAM role has the minimum required permissions
aws iam get-role-policy --role-name terraform-apply-production --policy-name state-access

For a Terraform Cloud setup:

# Audit workspace access
tfc organization audit -json | jq '.workspaces[] | {name, access: .access}'

Production failure modes

Symptom: state bucket has public read access. Cause: S3 bucket policy misconfiguration; ACL; bucket-level Block Public Access disabled. Recovery: enable Block Public Access at the account level; remove public policies; rotate any IAM credentials that may have been exposed; review access logs for the exposure window.

Symptom: a developer laptop has the production state in a local file. Cause: an operator ran terraform init against production without the remote backend; the state file landed locally. Recovery: configure the remote backend; run a refresh-only plan to push state; delete the local file; rotate any credentials that were on the laptop.

Symptom: KMS key policy allows access from a wrong account. Cause: the key policy was written too broadly during setup. Recovery: tighten the key policy; rotate the key; re-encrypt the state with the new key.

Symptom: the apply IAM role has been over-permissioned with * resources. Cause: the policy was written for convenience during initial setup. Recovery: rewrite the policy with the minimum resources; re-test applies.

Recovery

A state security incident is covered in its own lesson. The short version:

  1. Stop all applies. The state is compromised.
  2. Identify the exposure: who had access, what they could read, what they could write.
  3. Rotate any credentials that touched the exposed state.
  4. Restore state from a clean backup if integrity is in doubt.
  5. Audit the access logs for the exposure window.
  6. Post-incident review: how did the exposure happen, what control failed, what changes prevent recurrence.

What comes next

The next lesson covers sensitive = true — the affordance for marking values that should be suppressed in CLI output, and its limitations.

Verification

  • You can list the categories of sensitive data in state (IDs, ARNs, network metadata, secrets).
  • You can name the three roles (read, write, delete) and the blast radius of each.
  • You can explain why production and non-production state must live in separate backends.
  • You can write an IAM policy that grants the minimum state access for an apply role.

Knowledge check · 7 questions

  1. Q1. Which of these is the most severe consequence of state read access by an unauthorised party?

  2. Q2. Production and non-production state belong in separate buckets, because a shared bucket is a shared blast radius even when the prefixes differ.

  3. Q3. Who should hold the apply IAM role for production Terraform?

  4. Q4. What is the right way to handle a secret that Terraform needs at apply time (for example, a database password)?

  5. Q5. Which of the following are in state and should be considered sensitive? (Select all that apply.)

  6. Q6. An S3 bucket that holds state files has a policy that grants `s3:GetObject` to `arn:aws:s3:::tfstate-*/*`. What is the production concern?

  7. Q7. An ex-engineer retained an active IAM access key with s3:PutObject on the production state bucket. The access was not revoked on their departure. What is the immediate risk?

Passing score: 75%. Answers are checked in this browser.