Skip to main content
RunBook Academy

TerraformXI · State Security and LifecycleProduction Terraform

Sensitive Values in State and Variables

Intermediate⏱ ~12 minbash

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

Not yet marked complete on this device.

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

SurfaceRedacted by sensitive = true?
Human-readable CLI outputYes
State file (terraform state pull)No
Plan file (terraform show -json plan.tfplan)No
terraform output -json value fieldNo
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

  1. Identify the secret that leaked. Rotate it in the source of truth (Secrets Manager, Vault, etc.).
  2. Audit the surfaces where the secret may have been disclosed (logs, plan files, state backups, debug output).
  3. For each surface: revoke access, delete the artefact, rotate again.
  4. Mark the variable/output as sensitive and re-apply.
  5. For long-term protection, fetch from a secrets manager and use write_only where 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 = true does 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_only on an attribute that supports it (Terraform 1.9+).

Knowledge check · 7 questions

  1. Q1. What does `sensitive = true` do?

  2. Q2. What is the production pattern for managing a database password that Terraform needs at apply time?

  3. Q3. A variable marked `sensitive = true` is still written to the state file in plain text.

  4. Q4. Which Terraform version introduced the write_only argument?

  5. Q5. Which surfaces can still disclose a secret marked `sensitive = true`? (Select all that apply.)

  6. Q6. An operator runs `terraform output -raw database_password` and the password is printed to the terminal. What is the production concern?

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