Secrets and Sensitive Inputs
Real infrastructure needs secrets — database passwords, API keys, TLS private keys — and Terraform makes it easy to handle them badly: generated into state, passed as plain variables, printed in outputs. The right pattern is the opposite of ownership. Keep the secret value in a dedicated store, have the application read it at runtime, and let Terraform touch the value as little as possible.
The reason is the state file. Anything Terraform generates or reads lands in state in plaintext, regardless of how it's marked in config — a point from Chapter 3 that becomes operational here. sensitive = true hides a value from CLI output; it does not encrypt the bytes on disk. So the question for every secret is not "how do I mark it sensitive" but "can I keep it out of state entirely".
Where Secrets Come From
A secret arrives one of three ways. Terraform generates it (a random_password for a fresh database), an operator supplies it (an externally-issued API key), or a secrets manager already owns it and the value is never yours to hold. The first is the most dangerous, because the generated value lives in state forever; the third is the goal, because the value never enters Terraform at all.
Secrets Manager and SSM Parameter Store
The pattern that keeps the value out of code is to provision the secret container in Terraform — the aws_secretsmanager_secret or SSM parameter — while keeping the value out of the config. Terraform creates the empty secret and the IAM grants around it; the value is written separately by an operator, a CI step, or a rotation Lambda. The container is infrastructure; the value is not.
resource "aws_secretsmanager_secret" "db_password" { name = "prod/app/db-password" recovery_window_in_days = 7 } # The application reads this at runtime via its IAM role — # Terraform never holds the plaintext value.
Notice what's absent: there is no secret_string here. Terraform owns the secret's existence, its name, and (through IAM, as in the previous topic) who may read it — but the plaintext never enters the config or the state. The application's instance role grants secretsmanager:GetSecretValue, and the app fetches the value when it starts.
Referencing vs Owning
The architectural pivot is referencing the secret instead of owning it. When the app reads the password from Secrets Manager at boot, the value lives in exactly one place and never transits Terraform's state. The anti-pattern is having Terraform read the secret — with the aws_secretsmanager_secret_version data source — only to pass it into another resource's argument. That read drags the plaintext into state for no reason the runtime path couldn't have served directly.
The State Exposure Problem
When Terraform must generate a secret, accept that it is now in state in plaintext and protect it accordingly. The random_password below is a legitimate use — you need a strong password and don't want a human to choose it — but its result is written to state the moment it's created. The mitigation is entirely at the state layer: an encrypted, tightly-locked backend, and a plan to rotate the value out of Terraform's hands.
resource "random_password" "db" { length = 32 special = true } resource "aws_db_instance" "app" { identifier = "app-prod" engine = "postgres" instance_class = "db.t3.medium" username = "appuser" # both this and random_password.db.result are now in state, plaintext password = random_password.db.result } output "db_endpoint" { value = aws_db_instance.app.endpoint # NOT the password — never output a secret without sensitive = true }
The output deliberately exposes the endpoint, not the password. Outputting a secret without sensitive = true prints it straight into CI logs, which are retained and searchable — a leak that outlives the run. Mark every secret variable and output sensitive so it stays out of the console and the logs.
Passing Secrets In
For an operator-provided secret, pass it through a TF_VAR_ environment variable backed by a CI secret store, never a committed .tfvars file or a variable default. A secret in a default or a tracked tfvars is in version control the moment you push, and sensitive = true does nothing about that — it's already in the git history. The environment-variable path keeps the value out of the repository and lets CI inject it from a vault at run time.
- Generating a database password with
random_passwordand leaving it in unencrypted state with broad bucket access, so anyone with state read has the password in plaintext. - Committing a secret in a
.tfvarsfile or a variable default, putting it in git history wheresensitive = truecan't reach it. - Outputting a secret without
sensitive = true, printing it into CI logs that are retained and searchable long after the run. - Reading a secret with the version data source just to pass it into another argument, dragging the plaintext through state when a runtime reference would have avoided it.
- Believing
sensitive = trueencrypts the value — it only suppresses CLI display; the bytes in state are still plaintext.
- Keep secret values in Secrets Manager or SSM and have the application read them at runtime through its IAM role — not Terraform.
- When Terraform must generate a secret, store state encrypted (S3 with KMS), lock the bucket to the smallest principal set, and plan to rotate.
- Supply operator-provided secrets via
TF_VAR_environment variables from a CI secret store, never committed files or defaults. - Mark every secret variable and output
sensitive = trueso it stays out of console output and logs. - Provision the secret container in Terraform but write the value out of band, so the plaintext never enters config or state.
Knowledge Check
Why is referencing a secret at runtime preferred over having Terraform own its value?
- The plaintext never enters Terraform's state, where it would otherwise sit unencrypted
- Runtime references make the apply run measurably faster than generating the secret in Terraform
- Terraform is unable to create an
aws_secretsmanager_secretresource at all - Referencing the value causes Terraform to automatically rotate the secret on every apply
What does sensitive = true actually do to a value?
- Suppresses it from plan, apply, and output display — but the bytes in state stay plaintext
- Encrypts the value at rest with a project key wherever it is written into the state file on the backend
- Prevents the value from ever being written to state
- Stores the value in Secrets Manager instead of state
What is the right way to pass an operator-provided secret into a Terraform run?
- A
TF_VAR_environment variable backed by a CI secret store, never a committed file or default - A
terraform.tfvarsfile committed to the repo withsensitive = trueset - A variable
defaultin the code, since defaults aren't shown in plan output - Hardcoded directly in the resource argument and then masked from the plan output with
sensitiveset
You generate a database password with random_password. What must you do about it?
- Treat state as holding the plaintext: encrypt the backend, lock it down, and plan to rotate
- Nothing —
random_passwordis designed to keep the generated value out of the state file - Output the value so it is recorded somewhere retrievable for the application to read later
- Commit it to a tfvars file so the same password is reused across applies
You got correct