Chapter 8: Managing Real Infrastructure
Topic 50

Secrets and Sensitive Inputs

Secrets

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".

Two ways Terraform touches a secret
Terraform owns it
random_password generates the value, or the version data source reads it. Either way the plaintext lands in state — protect the backend and plan to rotate. The dangerous path.
Terraform references it
Terraform provisions the empty container and the IAM grants; the app reads the value from Secrets Manager or SSM at runtime through its role. The plaintext never enters state. The goal.

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.

secrets.tf — provision the container, not the value
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.

db.tf — a generated password lands in state; protect the backend
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.

Common Mistakes
  • Generating a database password with random_password and leaving it in unencrypted state with broad bucket access, so anyone with state read has the password in plaintext.
  • Committing a secret in a .tfvars file or a variable default, putting it in git history where sensitive = true can'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 = true encrypts the value — it only suppresses CLI display; the bytes in state are still plaintext.
Best Practices
  • 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 = true so 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.
Comparable tools CloudFormation dynamic references to Secrets Manager and SSM avoid embedding secrets Pulumi encrypts secrets in its state by default Vault a common external secrets backend across all of them

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_secret resource 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.tfvars file committed to the repo with sensitive = true set
  • A variable default in the code, since defaults aren't shown in plan output
  • Hardcoded directly in the resource argument and then masked from the plan output with sensitive set

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_password is 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