TerraformXIII · Variables, Outputs, and LocalsProduction Terraform
Sensitive Values and the Production Interface
What you'll learn
- Mark variables, outputs, and resource attributes sensitive when warranted
- Predict what `sensitive = true` hides and what it does not
- Source secrets from a secrets manager via a `data` source, not from tfvars
- Audit a configuration for surfaces that still expose sensitive values
- Rotate secrets after any incident involving exposure
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
A database password sits in a Terraform variable. The operator types
terraform apply and the password scrolls past in the plan output, into
the CI log, into the Slack alert, into the engineer’s terminal
scrollback. The sensitive = true flag is the smallest defensive layer
that prevents that scroll. It is not encryption. It is not access
control. It is the difference between a leak you can trace and a leak
that goes everywhere.
A real production incident: the team marked db_password as sensitive
but used it as the password argument to aws_db_instance. The apply
succeeded. A week later, an engineer ran terraform state pull to
debug a different issue and the password appeared in the engineer’s
terminal. The flag suppressed the CLI output; it did nothing to the
state file.
What sensitive = true hides
The flag is a UI feature. It tells the Terraform CLI and the UI to redact the value wherever it would otherwise be rendered.
# CONFIGURATION
variable "db_password" {
type = string
sensitive = true
description = "Master password for the production RDS instance."
}
resource "aws_db_instance" "prod" {
engine = "postgres"
engine_version = "16.3"
username = "admin"
password = var.db_password
tags = {
Environment = "production"
}
}
Plan output (READ-ONLY):
# aws_db_instance.prod will be created
...
password = (sensitive value)
username = "admin"
...
Plan: 1 to add, 0 to change, 0 to destroy.
The (sensitive value) placeholder appears in:
terraform planoutputterraform applyoutput (after the change is applied)terraform outputfor sensitive outputsterraform consolewhen the value would be displayed- The UI of Terraform Cloud / Enterprise (where supported)
What sensitive = true does NOT hide
The flag is a UI redactor. It is not encryption. It is not access control. The value persists in plaintext in several places:
| Surface | Plaintext? | Why |
|---|---|---|
| State file | yes | State records the actual value for drift detection |
| Cloud provider’s view of the resource | yes | The password is set on the database |
local-exec / remote-exec stdout | yes | The provisioner writes whatever it writes |
terraform state pull | yes | Dumps the raw state |
terraform console var.db_password | yes | The console evaluates the expression |
| Other tools that touch the state | yes | Anyone with read access to state |
# READ-ONLY — proves the value is in state
terraform show -json | jq '.resources[]
| select(.type == "aws_db_instance")
| .instances[].attributes.password'
"the-actual-password-in-plaintext"
Source secrets from a secrets manager
The discipline is to never put a secret in the configuration in the first place. Fetch it at apply time from a manager you control.
# CONFIGURATION
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/db/master"
}
resource "aws_db_instance" "prod" {
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# ...
}
output "db_endpoint" {
value = aws_db_instance.prod.endpoint
sensitive = true
}
The data source does not log the value. The apply output redacts it
because the resource attribute that consumes it is sensitive (the AWS
provider marks password sensitive automatically). The state file
contains the value; the encryption-at-rest of the state backend is the
real control.
WhyThisMatters
WhyThisMatters A sensitive flag on a variable is the smallest
defensive layer. The real production pattern is: secrets manager for
storage, IAM for access, encryption-at-rest for state, and rotation as
a runbook. The flag is the difference between a leak that goes to a
terminal scrollback and a leak that is logged in three systems.
Failure modes
-
local-execwrites the value. A provisioner logsvar.db_passwordto a file. Thesensitiveflag does nothing — the value is in the file in plaintext. -
Sensitive variable feeds a non-sensitive output. The output propagates the value into another stack that does not mark the input sensitive. The downstream plan renders the value.
-
State file in a public S3 bucket. Anyone with the URL reads
terraform.tfstateand decrypts nothing — the values are plaintext. -
terraform consolereads the variable. The CLI prints the value to the operator’s terminal. The flag is bypassed. -
Module passes sensitive to non-sensitive child input. The child module’s plan renders the value unless the child input is also marked sensitive.
-
-var-filewith a sensitive value committed to Git. The file lands in the repository. The flag does not retroactively un-commit the value. -
terraform output -jsonpiped to a logger. The JSON contains(sensitive value)as a marker, but-rawmode on a sensitive output renders the value. A script that pipes-rawto a log leaks.
Audit checklist
When a sensitive value is in scope, the audit answers five questions:
- Source. Where does the value come from? If
.tfvarsin Git, the audit fails immediately. - Storage. Is the state file encrypted at rest? Who can read it?
- Propagation. Does the value flow into a non-sensitive downstream input? Mark every consumer sensitive.
- Provisioners. Does any
local-execorremote-execwrite the value to a file, log, or API call? Remove the write or mask the value. - Rotation. Is the secret in a rotation runbook? After any exposure, the answer is “rotate now.”
Production guidance
- Mark variables sensitive when they hold secrets. Do not mark variables sensitive “just in case” — the flag suppresses useful plan output and adds friction to debugging.
- Mark outputs sensitive when they expose the same secrets (database endpoints, API keys).
- Never store secrets in
*.tfvars,terraform.tfvars, or-varCLI flags. The CI log is a public surface. - Read secrets via
datasources from a secrets manager you control. - Encrypt the state file at rest. Lock the state backend to the smallest possible set of IAM principals.
- After any incident involving exposure, rotate the secret. The flag does not make an exposed secret safe.
What comes next
The next lesson is Outputs: The Configuration Interface — the contract by which one Terraform stack hands values to another, and the sensitivity discipline that applies across stacks.
Verification
- A variable is marked
sensitive = true. Does the state file contain the value in plaintext? Why? - A
local-execprovisioner writesvar.db_passwordto a file. What does thesensitiveflag do in this case? - You mark a variable sensitive but feed it into a non-sensitive downstream module input. What happens in the downstream plan?
- Where in the production pipeline is the password stored when you
use
data "aws_secretsmanager_secret_version"?
Knowledge check · 7 questions
Q1. What does `sensitive = true` on a variable do?
Q2. The Terraform state file stores sensitive variables in encrypted form.
Q3. Where should production secrets such as database passwords come from?
Q4. A `local-exec` provisioner writes `var.db_password` to a log file. What does `sensitive = true` do in this case?
Q5. Which of the following surfaces still expose a sensitive value despite the `sensitive = true` flag? (Select all that apply.)
Q6. Which command can bypass the `sensitive` suppression on a variable?
Q7. An auditor asks where the production database password lives. Which answer demonstrates a defensible posture?
Passing score: 75%. Answers are checked in this browser.