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:
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:
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
}
ephemeral "aws_secretsmanager_secret_version" "db" {
secret_id = "payments/prod/db-master"
}
resource "aws_db_instance" "orders" {
# ...
password_wo = ephemeral.aws_secretsmanager_secret_version.db.secret_string # write-only: sent, never stored
password_wo_version = 3 # bump to rotate
}
# Azure: Postgres Flexible Server with Entra ID authentication (no password), or a Key Vault secret
# read by the app at runtime instead of passed through Terraform.
# GCP: Cloud SQL IAM database authentication, or Secret Manager accessed by the workload's service account.
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 planthrough 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_keyarguments fed by variables.
Key points
sensitive = truehides 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.