Skip to content
Infrastructure as Code iac security 5 min read

Protecting State

Terraform’s state file is the most security-sensitive artifact in your entire workflow. It records the real-world mapping of every resource you manage — and crucially, it stores resource attributes in cleartext, including any database password, private key, or API token that flowed through your configuration. The sensitive = true flag redacts those values from CLI output but does nothing to the bytes on disk. Securing state is therefore not optional: it is the single most important control for keeping infrastructure secrets out of the wrong hands.

Why state contains secrets

When Terraform creates an RDS instance, an IAM access key, or a TLS certificate, it writes the returned attributes back into state so it can detect drift on the next plan. There is no way to opt out of this — the value must be persisted to be managed. You can confirm it directly:

terraform state pull | jq '.resources[].instances[].attributes.password'

Output:

"S3cr3t-Pa55word!"

Anyone who can read the state file can read every secret it contains, regardless of how carefully you marked variables sensitive. Local terraform.tfstate files are especially dangerous because they sit unencrypted in your working directory.

Never commit state to git

The first and easiest mitigation is to keep state out of version control entirely. A committed terraform.tfstate exposes secrets to everyone with repository access and survives forever in git history. Add it — and its backup — to .gitignore from day one.

# .gitignore
*.tfstate
*.tfstate.*
.terraform/
*.tfvars

If you ever accidentally commit state, rotating the leaked credentials is mandatory — scrubbing git history is not enough, because the secret was exposed the moment it was pushed.

Use an encrypted remote backend

The right place for state is a remote backend that provides encryption at rest, access control, and locking in one package. On AWS, an S3 bucket with server-side encryption is the standard choice. Modern Terraform (1.6+) and OpenTofu support native S3 state locking via the use_lockfile argument, so a separate DynamoDB table is no longer required.

terraform {
  backend "s3" {
    bucket       = "acme-tf-state-prod"
    key          = "network/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    kms_key_id   = "arn:aws:kms:us-east-1:111122223333:key/abcd-1234"
    use_lockfile = true
  }
}

Setting encrypt = true enables SSE-S3 (AES-256); supplying kms_key_id upgrades this to SSE-KMS, which gives you an auditable, rotatable key and CloudTrail records of every decrypt call. Enforce encryption at the bucket level too, so no object can ever land unencrypted:

resource "aws_s3_bucket" "state" {
  bucket = "acme-tf-state-prod"
}

resource "aws_s3_bucket_versioning" "state" {
  bucket = aws_s3_bucket.state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
  bucket = aws_s3_bucket.state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.state.arn
    }
  }
}

resource "aws_s3_bucket_public_access_block" "state" {
  bucket                  = aws_s3_bucket.state.id
  block_public_access     = true
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Versioning lets you recover from a corrupted or accidentally deleted state, and the public access block guarantees the bucket can never be exposed to the internet.

Restrict who can read state

Encryption at rest protects against someone stealing the disk; it does nothing against an over-privileged IAM principal. Reading state must be treated as equivalent to reading every secret in your infrastructure, so scope backend access to the smallest possible set of identities — typically a CI/CD role and a break-glass admin.

resource "aws_s3_bucket_policy" "state" {
  bucket = aws_s3_bucket.state.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid       = "DenyNonTLS"
      Effect    = "Deny"
      Principal = "*"
      Action    = "s3:*"
      Resource  = ["${aws_s3_bucket.state.arn}", "${aws_s3_bucket.state.arn}/*"]
      Condition = { Bool = { "aws:SecureTransport" = "false" } }
    }]
  })
}

For KMS-encrypted state, the KMS key policy is your real access boundary: a principal needs both s3:GetObject and kms:Decrypt to read state, giving you two independent gates to audit.

Locking to prevent corruption

State locking is a security and integrity control: it stops two concurrent applies from racing and writing a half-merged state, which can orphan resources or silently revert changes. With use_lockfile = true, the S3 backend writes a .tflock object alongside your state; remote backends like Terraform Cloud and Vault-backed setups lock automatically.

Output:

Acquiring state lock. This may take a few moments...
Error: Error acquiring the state lock

Lock Info:
  ID:        9f3c1a2b-...
  Operation: OperationTypeApply
  Who:       ci-runner@build-7421
  Created:   2026-06-14 09:12:04 UTC

If a process crashes and leaves a stale lock, release it deliberately with terraform force-unlock <LOCK_ID> — never by editing the backend by hand.

Backend options at a glance

BackendEncryption at restLockingAccess control
S3 + KMSSSE-KMS (auditable, rotatable)Native use_lockfileIAM + KMS key policy
Azure BlobStorage Service EncryptionNative (blob lease)Entra ID RBAC / SAS
GCSDefault + optional CMEKNativeIAM
Terraform CloudEncrypted, managedAutomaticWorkspace teams/RBAC
Local fileNoneLocal file lockFilesystem only

OpenTofu is a drop-in replacement for the Terraform CLI and supports all of these backends identically. It additionally offers client-side state encryption, which encrypts the state payload with a passphrase or KMS key before it reaches the backend — defense in depth for teams who don’t fully trust the storage layer.

Best Practices

  • Treat the state file as a top-tier secret: anyone who can read it can read every credential your infrastructure uses.
  • Always use a remote backend with encryption at rest — prefer SSE-KMS over SSE-S3 for auditability and key rotation.
  • Add *.tfstate* and .terraform/ to .gitignore, and rotate any credential that was ever committed.
  • Enable bucket versioning and a public access block so state can be recovered but never exposed.
  • Scope backend IAM and KMS key policies to the minimum set of CI roles and break-glass admins.
  • Enforce locking (use_lockfile = true or a managed backend) to prevent concurrent applies from corrupting state.
  • Consider OpenTofu client-side state encryption when the storage backend is outside your trust boundary.
Last updated June 14, 2026
Was this helpful?