Safeguard
Cloud Security

Your Terraform State Is a Secrets Store With a Build Artefact's Access Control

The database password, the generated private key, the API token an output exposed. You did not put them there directly. Terraform did, because it needs the full attributes of everything it manages to plan the next change.

Tomas Lindgren
Platform Engineer
5 min read

Your Terraform state file has plaintext secrets in it: the database password you passed as a variable, the private key you generated with a resource, the API token an output exposed for a downstream module to consume.

You did not put them there directly. Terraform did, because it has to know the full attributes of everything it manages to plan the next change, and a password is an attribute like any other.

This post is where they end up and what to do about it. For whoever owns the state backend.

Why the secret is in the state at all

Terraform's model requires it. To compute a diff, it needs the actual value of every attribute of every resource it manages, not a reference to where the value lives. If a resource has a password argument, whatever you passed becomes part of the state, in the clear, because Terraform has no concept of a field that participates in planning but is never stored.

This is true whether the secret came from a variable, was generated by the provider, such as an RDS master password, or was read from another secret store entirely. Reading a secret from your vault into a Terraform variable does not protect it: it now lives in two places, one of which is state.

Where the copies are

The primary state file, wherever your backend keeps it: a storage bucket, a database, a SaaS backend. Read access to the backend is read access to every secret Terraform has ever managed.

State history. Most backends version state, which means an old secret remains recoverable in a prior version even after rotation. Rotating in the infrastructure and forgetting the history means the compromised value is still retrievable.

Local copies. Anyone who ran terraform plan or apply from their laptop, without a remote backend enforced, has a local state file, and a local state file is a laptop-portable copy of every secret in your infrastructure.

CI logs. A terraform plan output can print changed values, including secrets, directly into a CI log that is retained, searchable, and often more widely readable than the backend itself.

Downstream consumers. An output marked as feeding another module or a CI variable propagates the value further, and each hop is a place the secret now lives that is not the backend.

What to do

Move the backend to something access-controlled, encrypted, and audited, never local files or a shared network drive. Object storage with encryption and access logging, or a purpose-built Terraform backend, both work. The question worth asking of yours: who can read it, and would you know if they did.

Mark sensitive outputs, so Terraform at least suppresses them from its own console output:

output "db_password" {
  value     = aws_db_instance.main.password
  sensitive = true
}

This stops casual display. It does not encrypt the value in the state file, which is the part people mistake it for. The state itself still contains the plaintext, readable by anything with backend access.

Prefer resources that do not put the secret in state at all. Where a provider supports it, generate the secret outside Terraform and store only a reference: an ARN, a secret name, a version identifier. Terraform manages the pointer; a secrets manager holds the value. This is the only approach that removes the secret from state rather than merely hiding it from output.

Enable encryption at rest on the backend, and separately confirm who holds the decryption key and whether their access is audited. This is the same distinction covered in the general encryption-at-rest discussion: it protects the stored file, not access through the normal path.

Restrict CI logging of plan output, or redact known-sensitive resource types before they reach a log a wider audience can search.

Rotate anything already exposed. If state has ever been in a less-controlled location, in a local checkout, in an old CI log, in a version of the backend from before you tightened it, treat every secret it might have contained as compromised and rotate. State age and reach are usually worse than anyone assumes on first look.

Check yours

# What is actually in your state right now?
terraform show -json | jq -r '.. | objects | to_entries[] | select(.key | test("password|secret|token|key"; "i")) | .key' | sort -u

# Who can read the backend?
# For an S3 backend, for example:
aws s3api get-bucket-policy --bucket your-tfstate-bucket
aws s3api get-bucket-encryption --bucket your-tfstate-bucket

# Does state versioning retain old secrets?
aws s3api list-object-versions --bucket your-tfstate-bucket --prefix path/to/state

The first command is worth running today. It finds fields matching a secret-shaped name across everything Terraform currently tracks, and the list is usually longer than whoever wrote the configuration remembers.

The concession

Removing every secret from state entirely is not always possible: some providers only expose a resource's generated credential as a plain attribute, with no reference-based alternative. In those cases the backend's access control and encryption are the only real control, and they need to be treated as protecting a secrets store, not a build artefact.

The proportionate response is to fix the backend access control unconditionally, because it costs nothing and closes the largest exposure, and to migrate resource by resource toward reference-based secrets where your providers support it, prioritising the ones with the most consequential credentials.

The implication

State is a secrets store that most teams treat as a deployment artefact, with the access control of a build log rather than a vault.

Run the grep against your own state today. Whatever it finds is currently readable by everyone with backend access, and the backend's access list is very likely wider than your secrets manager's.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.