TerraformXI · State Security and LifecycleProduction Terraform
Sensitive Values in State and Variables
What you'll learn
- Use sensitive = true on variables and outputs to suppress values in CLI output
- Recognise the limits of sensitive = true: it does not encrypt, hash, or protect state-at-rest
- Plan secret storage: fetch secrets from a secrets manager at apply time, do not let them flow through state
- Recognise the cost of forgetting sensitive = true (log 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
terraform apply prints a database password to the terminal. The
operator scrolls past it. The CI logs the output. The log is
indexed by the log search. Six months later, the security team
finds the password in a log archive. This is the most common
secret-disclosure incident in Terraform projects, and it is
entirely preventable.
What sensitive = true does
The sensitive = true flag suppresses a value in CLI output. That
is the entire effect.
variable "database_password" {
type = string
sensitive = true
}
output "database_endpoint" {
value = aws_db_instance.primary.endpoint
sensitive = false
}
When the variable is referenced in the plan, Terraform prints
(sensitive value) instead of the password. The value is still
in state in plain text. The value is still in the plan file. The
value is still in the JSON plan output (terraform show -json).
Where sensitive matters, and where it does not
| Surface | Redacted by sensitive = true? |
|---|---|
| Human-readable CLI output | Yes |
State file (terraform state pull) | No |
Plan file (terraform show -json plan.tfplan) | No |
terraform output -json value field | No |
Debug logs (TF_LOG=DEBUG) | No |
| Backend storage (S3 object) | No |
The flag is a thin redactor over the human-readable CLI stream. Nothing more. Production code that handles secrets must assume the value is visible in every other surface.
The right pattern: secrets manager
A database password is not stored in Terraform. It is fetched from a secrets manager at apply time.
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/database/password"
}
resource "aws_db_instance" "primary" {
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.medium"
username = "app"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
skip_final_snapshot = true
}
The password is fetched from AWS Secrets Manager. Terraform
holds it in memory for the duration of the apply. The password
is in state (because the password attribute is part of the
aws_db_instance schema), but the source of truth is Secrets
Manager. A rotation in Secrets Manager produces a diff on the
next plan; the team can re-apply to push the rotation through.
For passwords that should never be in state — a CI token, a third-party API key — the pattern is to pass them as a write-only argument:
resource "aws_ssm_parameter" "api_key" {
name = "/production/api-key"
type = "SecureString"
value = var.api_key
}
The variable is sensitive; the value flows into the SSM
parameter; the value is in state. To avoid the value being in
state, use write_only (Terraform 1.9+) or apply via a separate
mechanism.
Terraform 1.9 write-only attributes
Terraform 1.9 introduced write_only for resource arguments. An
argument marked write_only is sent to the provider on create
and update but is not stored in state.
resource "aws_ssm_parameter" "api_key" {
name = "/production/api-key"
type = "SecureString"
value = var.api_key
write_only = true # Terraform 1.9+
}
The value is sent to AWS on apply. The state stores only a
boolean write_only indicator. The next plan will not show the
value as a diff. Rotation requires the operator to re-apply with
a new value (or to update directly via the provider API).
write_only is provider-specific. Each provider decides which
arguments are eligible. Check the provider documentation; not
every sensitive attribute supports write_only.
Sensitive outputs
The same flag applies to outputs:
output "database_password" {
value = aws_db_instance.primary.password
sensitive = true
}
terraform output database_password
# database_password = (sensitive value)
terraform output -raw database_password
# <value>
# Warning: Output is sensitive; value will be displayed unredacted
The -raw flag returns the value but prints a warning. The
production discipline: capture the value into a variable and pass
it to the next command; never echo to a log.
Common leaks
Three patterns that leak secrets even with sensitive = true:
1. JSON plan output. terraform show -json plan.tfplan
includes the value. A CI pipeline that archives plan files to S3
archives the secret.
2. Debug logs. TF_LOG=DEBUG logs every value, sensitive or
not. Use TF_LOG=INFO or WARN in production.
3. State file backups. The S3 bucket that holds state also
holds the secret in plain text. Encrypt the bucket with KMS; lock
down access; do not grant s3:GetObject to anyone who does not
need it.
Validation
READ-ONLY
# Confirm the sensitive variable is redacted in plan output
terraform plan -var='database_password=hunter2' | grep password
# password = (sensitive value)
# Confirm the value is still in the state (the flag does not protect state)
terraform state pull | jq '.resources[] | select(.type == "aws_db_instance") | .instances[].attributes.password'
# "hunter2"
The grep confirms CLI redaction. The jq confirms state disclosure. Both are correct behaviour; the lesson is that state is not the place to keep secrets.
Production failure modes
Symptom: a secret appears in a CI log archive. Cause: a
variable or output that holds a secret was not marked
sensitive = true. Or the secret flowed through an unsensitive
path. Audit the configuration; mark everything sensitive; re-apply.
Symptom: a secret is in the state file. Cause: the secret
flowed through Terraform as a resource attribute (e.g.
aws_db_instance.password). The secret is in state by design.
The mitigation is to fetch from a secrets manager and accept that
state holds the value during the apply window. For long-term
storage, use write_only (Terraform 1.9+).
Symptom: terraform output -json returns null for an output
that should have a value. Cause: the output is marked
sensitive = true. -json returns null for sensitive values to
prevent serialisation into a log. Use -raw if the value is
needed.
Symptom: a plan output is archived to S3 and includes a secret. Cause: the plan was generated without sensitive redaction in JSON form. The JSON plan output includes all values, sensitive or not. The mitigation: encrypt the plan archive with KMS; lock down access; never let the plan archive leave the secured environment.
Symptom: rotation of a secret in Secrets Manager produces no
diff. Cause: the secret was passed as a write-only argument
(write_only = true); Terraform does not see the value in state
and cannot diff. Rotation requires re-applying with the new value
or rotating via the provider API.
Recovery
- Identify the secret that leaked. Rotate it in the source of truth (Secrets Manager, Vault, etc.).
- Audit the surfaces where the secret may have been disclosed (logs, plan files, state backups, debug output).
- For each surface: revoke access, delete the artefact, rotate again.
- Mark the variable/output as sensitive and re-apply.
- For long-term protection, fetch from a secrets manager and
use
write_onlywhere supported.
What comes next
The next lesson covers encryption at rest: how the state file is encrypted in the backend, and the production pattern for KMS key management and rotation.
Verification
- You can explain what
sensitive = truedoes and does not do (CLI redaction only; no encryption or state protection). - You can write a configuration that fetches a secret from AWS Secrets Manager.
- You can name the three common leak surfaces (JSON plan, debug logs, state backups).
- You can use
write_onlyon an attribute that supports it (Terraform 1.9+).
Knowledge check · 7 questions
Q1. What does `sensitive = true` do?
Q2. What is the production pattern for managing a database password that Terraform needs at apply time?
Q3. A variable marked `sensitive = true` is still written to the state file in plain text.
Q4. Which Terraform version introduced the write_only argument?
Q5. Which surfaces can still disclose a secret marked `sensitive = true`? (Select all that apply.)
Q6. An operator runs `terraform output -raw database_password` and the password is printed to the terminal. What is the production concern?
Q7. A team needs to set a sensitive parameter in AWS Systems Manager Parameter Store. The value should not be in state. Which pattern is correct?
Passing score: 75%. Answers are checked in this browser.