Terraform in Production · State You Can Trust

Secrets in State

sensitive = true hides values from the console, not from state. Keep secrets out of state, and treat state as a secret.

Scenario

Example team: the payments team creates its Postgres database with Terraform. The master password is a variable marked sensitive, passed from the CI secret store. A contractor gets read access to the state bucket "to debug plans".

What you see in production

A security review finds the production database password in plaintext in payments/prod/terraform.tfstate and in every old version of the file kept by bucket versioning. The CI logs show <sensitive>, so everyone assumed the value was protected.

The naive code

variable "db_password" {
  type      = string
  sensitive = true
}

resource "aws_db_instance" "orders" {
  identifier        = "orders-prod"
  engine            = "postgres"
  instance_class    = "db.t4g.medium"
  allocated_storage = 50
  username          = "app"
  password          = var.db_password   # stored in state
}

Reproduce it

The same pattern with a provider-free resource. First, what you see in the terminal:

What apply shows
variable "db_password" {
  type      = string
  sensitive = true
  default   = "s3cr3t-Pa55"
}

resource "terraform_data" "db" {
  input = { user = "app", password = var.db_password }
}

output "password" {
  value     = terraform_data.db.output.password
  sensitive = true
}

Output

password = <sensitive>

Run this on your computer — see “Get Started” for setup.

Then what is written to terraform.tfstate:

What the state file stores
variable "db_password" {
  type      = string
  sensitive = true
  default   = "s3cr3t-Pa55"
}

resource "terraform_data" "db" {
  input = { user = "app", password = var.db_password }
}

Output

terraform_data.db: {"input": {"password": "s3cr3t-Pa55", "user": "app"}, "output": {"password": "s3cr3t-Pa55", "user": "app"}}

Run this on your computer — see “Get Started” for setup.

Root cause

sensitive is a display feature: it redacts values in plan and apply output. State must record every attribute a provider returns so the next plan can compare against it, so the value is stored as it is. Anyone who can read the state, or any old version of it, can read the secret.

The fix

Keep the secret out of Terraform entirely, or use values that are never persisted:

resource "aws_db_instance" "orders" {
  identifier                  = "orders-prod"
  engine                      = "postgres"
  instance_class              = "db.t4g.medium"
  allocated_storage           = 50
  username                    = "app"
  manage_master_user_password = true   # generated and rotated in Secrets Manager; never in state
}

And treat state as sensitive data anyway: encryption at rest with a customer-managed key, read access only for the CI role and break-glass admins, and no state files on laptops or in CI artifacts.

Trade-offs

  • Provider-managed secrets (RDS + Secrets Manager) are the simplest and rotate themselves, but your app must read the secret at runtime.
  • Write-only arguments need a provider that supports them and a version number to trigger rotation, because Terraform can't diff a value it never stored.
  • Tightening state access slows debugging; give engineers terraform plan through CI instead of raw state reads.

Detect it in production

  • Scan state files (current and old versions) for secret patterns in CI, just like you scan repositories.
  • Audit who read state objects (CloudTrail data events, storage logs).
  • Review new resources for password, secret, private_key arguments fed by variables.

Key points

  • sensitive = true hides output; it does not encrypt or omit state.
  • Prefer platform-managed secrets or identity-based auth; use ephemeral values and write-only arguments when Terraform must pass a secret.
  • State is a secret store by accident: encrypt it and lock down who can read it.

Practice questions

Does `sensitive = true` keep a value out of the state file?

Answer

No. It only redacts the value in CLI output. State stores every attribute in plaintext JSON so future plans can compare against it.

What are ephemeral values and write-only arguments?

Answer

Ephemeral values (variables, outputs and ephemeral resources) exist only during a run and are never written to plan or state files. Write-only arguments accept such values, send them to the provider and do not persist them; a companion version argument tells Terraform when to send a new value.

What's the simplest way to keep an RDS master password out of Terraform state?

Answer

Set manage_master_user_password = true so RDS generates the password and stores and rotates it in Secrets Manager. Applications read it from Secrets Manager at runtime.

Open in the app to mark lessons complete, take notes and track your progress.