Skip to content
Infrastructure · 5 min read

Terraform state is the crown jewel, so protect it like one

The claim Teams adopt infrastructure-as-code for the reproducibility and then store the one file that makes it reproducible — the state file — on a laptop, in a Git repository, or...

A Written by Administrator
Terraform state is the crown jewel, so protect it like one

The claim

Teams adopt infrastructure-as-code for the reproducibility and then store the one file that makes it reproducible — the state file — on a laptop, in a Git repository, or in a bucket anyone can read. That file is a complete map of your infrastructure and, worse, a plaintext record of many of its secrets. If you run Terraform or OpenTofu, how you store and lock the state is a more important decision than anything in your configuration.

What the state file actually contains

State is not just a list of resources. It is the mapping between your code and the real resources it created, including their current attributes — and Terraform writes those attributes verbatim, secrets included. A database password set through a resource, an access key generated for a service account, a private key, the initial admin credential: if it was an attribute of a managed resource, it is sitting in the state file in plaintext.

grep -o '"password":[^,]*' terraform.tfstate    # do not be surprised by the output

This single fact rules out three storage locations immediately: never a Git repository, because history is forever and public forks happen; never a laptop, because laptops are lost and imaged; never a bucket with broad read access, because "internal" is not "encrypted and audited".

The two problems remote state solves

Storing state in a shared, locking backend solves a security problem and a correctness problem at once.

The security problem is the plaintext secrets above: a proper backend encrypts state at rest and restricts who can read it. The correctness problem is concurrent modification. If two engineers run apply against the same infrastructure at the same time, and state lives on their two laptops, they will each make changes the other's state does not know about — and the result is duplicated resources, orphaned resources Terraform no longer tracks, and a state file that no longer matches reality. State locking makes the second apply wait for the first to finish.

A correct backend

terraform {
  backend "s3" {
    bucket         = "acme-tf-state"
    key            = "prod/terraform.tfstate"
    region         = "ca-central-1"
    encrypt        = true
    use_lockfile   = true          # native S3 locking
  }
}

Three properties make this safe. encrypt = true encrypts the object at rest. The bucket has versioning enabled, so a corrupted or accidentally deleted state can be rolled back to the previous version — state corruption is recoverable only if you kept the history. And locking is enabled, so concurrent applies serialise instead of colliding.

# bucket configuration, set once:
# - versioning: enabled  (state history / rollback)
# - encryption: enabled  (at rest)
# - public access: fully blocked
# - access: a single IAM role, not individual users

Keep secrets out of state where you can

Encryption at rest protects the file, but the best secret in a state file is the one that is not there. Two habits reduce the exposure. First, generate secrets outside Terraform and reference them — have the application or a secrets manager create the database password, and pass Terraform only a reference, not the value. Second, when Terraform must handle a secret, mark the variable sensitive = true so it is redacted from plan output and logs, which is where secrets leak into CI systems and terminal history even when the state file itself is well protected.

Neither habit removes secrets from state entirely — some resources return generated credentials as attributes no matter what — which is exactly why encryption and tight access on the backend remain non-negotiable.

The blast radius rule: split your state

A single state file for your entire infrastructure means a single mistake — or a single terraform destroy run against the wrong directory — can affect everything. Split state by blast radius: separate files for networking, for data stores, and for the application layer, so that an apply to the application cannot touch the database, and a corrupted application state does not take the network with it. The boundary to draw is around things that change at different rates and that you would never want to destroy together.

The recovery you should test before you need it

State can be lost or corrupted, and the recovery path is not obvious under pressure, so walk it once deliberately. Confirm you can restore a previous state version from the bucket's history. Confirm you can re-import an existing real resource into state with terraform import if the mapping is lost. And keep the backend configuration itself — the bucket, the lock setup — documented outside Terraform, because it is the one piece of infrastructure Terraform cannot bootstrap for you. The team that has restored state once, on a calm afternoon, recovers from the real incident in minutes; the team that never has spends the incident learning that terraform import exists.

#terraform #infrastructure as code #security #operations

Keep reading