Skip to content
intermediate Phase 11 · Policy Enforcement, Security Scanning, and Secrets

Sensitive Values and Redaction

Marking secrets sensitive and understanding what redaction does and does not hide.

50m
0 problems
Topic Progress 0%

Marking a Password sensitive and Reading Exactly What the Plan Shows

Mark a secret with sensitive = true, run terraform plan, and the password line reads (sensitive value). Your reviewer sees the same masked string on their screen. That is the entire promise of the flag, and the promise is narrower than most people assume.

variable "db_password" {
  type      = string
  sensitive = true
}

resource "aws_db_instance" "orders" {
  engine         = "postgres"
  instance_class = "db.t3.micro"
  username       = "ordersadmin"
  password       = var.db_password
}

output "endpoint" {
  value     = aws_db_instance.orders.address
  sensitive = true
}

Three mechanisms are at work, and each one shows up in interviews.

Where the flag can be set. sensitive = true is an argument of input variables and output values only. You cannot flag a resource attribute yourself; its sensitivity comes from the provider schema — the AWS provider declares aws_db_instance.password sensitive — or from propagation. locals have no sensitive argument at all: a local becomes sensitive purely by referencing a sensitive value.

Propagation. Since Terraform 0.14 the mark is viral. Any expression computed from a sensitive value is sensitive: concatenations, lookup(), jsonencode(), attributes built from it, and whole collections — mapping a splat over a list of secrets yields a sensitive list, so Splat and Attribute Access Expressions behaves identically with the mark attached. That is why the endpoint output above carries sensitive = true even though an address is not a secret: once any part of an output expression touches a sensitive value, Terraform refuses to render it unless the whole output is marked, failing with Error: Output refers to sensitive values. Where it does render, plan text shows # (sensitive value) and terraform console prints (sensitive value) in place of the real one.

What redaction actually covers. Exactly the surfaces the Terraform CLI renders for humans. Everything else receives the real value plus a hint telling it to mask if it wants to:

Surface Secret visible?
terraform plan / apply text output No — (sensitive value)
terraform console No
terraform output (plain text) No
terraform output -json Yes — plaintext with a sensitive flag
Saved plan file (plan -out tf.plan) Yes — full plaintext
terraform.tfstate and remote state Yes — full plaintext
terraform show -json tf.plan Yes — plus after_sensitive metadata

Read that table twice. sensitive is display redaction, not encryption, not access control, not deletion. The flag exists so a credential does not scroll past in a terminal recording or a CI log. It never changes what Terraform writes to disk, and it never restricts who may read state. Machine-readable formats carry plaintext deliberately: tools downstream need the value and are trusted to honor the flag.

One refusal is worth memorizing because it surprises people: a sensitive value cannot be a for_each or count key. Terraform stops with Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments, because instance keys land in resource addresses, diagnostics, and state — surfaces the CLI does not redact. If your map keys must be secret, the design is wrong, not the flag.

The next chapter walks each row of that table as a real leak path; after that, Terraform 1.10+ gives you values that never reach disk at all.

Where Redaction Ends: State, Plan Artifacts, JSON, and Debug Logs

Redaction ends where the CLI's rendering ends. Five leak paths, ordered by how often they actually bite in production teams.

1. State, always. After a successful apply the provider has sent the password to AWS, and Terraform records the prior value so future plans can compute a diff. The AWS provider documents this verbatim: "All arguments including the username and password will be stored in the raw state as plain-text." Terraform cannot diff a value it refused to store, so sensitive never touches state. The corrective action is protecting the backend: server-side encryption with a KMS key you control, a bucket policy restricting s3:GetObject on the state object to deploy roles, locking so two applies never race, and an audit trail on state reads. Treat everyone with state access as a secret reader.

2. The saved plan file. terraform plan -out tf.plan writes an opaque binary containing the full proposed configuration — every secret included — because terraform apply tf.plan must run without re-reading your variables. A CI job that archives tf.plan as a build artifact has created a secret store governed by artifact retention settings. Shorten retention, restrict who can download, delete after apply.

3. JSON output. terraform show -json tf.plan > plan.json emits planned_values holding the real values, with sensitive positions annotated under after_sensitive so consumers can mask. The values are there because machine consumers need them — policy engines like the ones you will meet in OPA and Conftest Policy Gates evaluate rules against the actual proposed arguments. Any pipeline that dumps plan JSON into logs, tickets, or PR comments leaks every credential the plan touched. Posting the human-readable plan instead does not fix it: a comment bot must consume JSON and apply its own redaction.

4. Provider debug logs. TF_LOG=DEBUG, or TF_LOG_PROVIDER=TRACE, writes provider RPC traffic to stderr — request bodies included. A password that never appears in a plan can appear twice in a debug trace. Turn tracing on locally for the one run you need, then delete the file. Never upload debug logs as CI artifacts.

5. Raw state access. terraform state pull, the backend's storage API, a terraform.tfstate copy left in a Downloads folder — every one of these shows plaintext. Sensitivity is metadata that only the CLI honors; the state API does not filter on it.

The audit question for any repository: who can read the state, and are they still supposed to have access six months later? If the answer includes everyone with repo access, or an account-wide S3 policy with hundreds of engineers in it, the sensitive flag is providing comfort rather than security. Fix state access first, then reduce how many secrets ever reach state — which is exactly what Injecting Secrets from External Stores is about, and what the ephemeral machinery in the next chapter makes nearly leak-proof.

Treat a plan file as sensitive even when terminal output redacts marked values. A saved binary plan can contain values needed for apply, and a JSON rendering may expose sensitive data in clear text. Restrict artifact access, retention, and downloads, and avoid attaching plan JSON to public pull requests. State backends need encryption at rest, TLS in transit, access controls, versioning, and audit logs. Redaction in a CLI is not a substitute for protecting every copy of state and plan data.

Beyond Redaction: Ephemeral Values and Write-Only Arguments

sensitive = true protects display. If the threat model is a leaked state file, you need values that never reach state at all. Terraform 1.10 and 1.11 added exactly that, and they are the answer interviewers expect when the follow-up is "so how do you keep the secret out of state?"

Ephemeral values (Terraform 1.10). A value marked ephemeral exists only for the duration of one Terraform operation and is never written to plan or state:

variable "vault_token" {
  type      = string
  sensitive = true
  ephemeral = true
}

ephemeral "random_password" "db" {
  length           = 20
  override_special = "!#$%&*()-_=+[]{}<>:?"
}

Ephemeral resources are re-evaluated in every phase, so plan and apply each get a fresh value — a 15-minute token from plan time may already be expired when apply runs, which is exactly why you fetch short-lived material this way instead of pinning it. The non-persistence is enforced, not best-effort: using an ephemeral value where Terraform must store something — a plain resource argument such as password — raises an error rather than silently writing state. Ephemeral values are allowed only in ephemeral-capable contexts: other ephemeral variables and outputs, provider and provisioner blocks, and (since 1.11) write-only arguments.

Write-only arguments (Terraform 1.11). Providers expose individual arguments that accept a value but are never read back:

resource "aws_db_instance" "orders" {
  engine              = "postgres"
  instance_class      = "db.t3.micro"
  username            = "ordersadmin"
  password_wo         = ephemeral.random_password.db.result
  password_wo_version = 1
}

The provider receives password_wo, calls the cloud API, and Terraform discards the value without storing it in plan or state. Nothing stored means nothing to diff — so updates are driven by the paired version argument instead. password_wo_version is recorded in state; incrementing it tells the provider to take the new password_wo value on the next operation and apply it. The pattern is spreading across providers: secret_string_wo on aws_secretsmanager_secret_version, master_password_wo on aws_rds_cluster, value_wo on aws_ssm_parameter. You can even chain it: generate with ephemeral.random_password, store via secret_string_wo, read back with an ephemeral data-form resource, and feed a database — the secret never lands in any artifact.

The escape hatch: nonsensitive(). It strips the mark from a value. Legitimate uses are rare and deliberate; wrapping a real credential in it converts a redacted value into a printed one. Treat every nonsensitive() call as a change that needs a written reason in review.

With ephemeral values plus write-only arguments, the leak table from the first chapter shrinks to nothing for the secret itself: state, plan files, and show -json output contain it by construction. What remains is the provider's own transport to the cloud API, and the credentials Terraform uses to reach that API in the first place — the problem Injecting Secrets from External Stores takes on next.

Sensitivity can propagate through expressions: if an output is calculated from a marked secret, Terraform keeps the result marked sensitive unless an author explicitly declassifies it. Use nonsensitive only when you have proved the resulting value is safe to display; stripping the mark does not transform or encrypt the value. In code review, trace the secret from its input through locals, resource arguments, outputs, state, and CI artifacts. Also remember that provider behavior and external logging can expose values independently of Terraform's display redaction.

Quiz

1. A teammate sets sensitive = true on an input variable and concludes the secret is now encrypted at rest. Which statement is accurate?

Question 1 options

2. An output block references aws_db_instance.orders.password and declares no sensitive argument. What happens on the next terraform plan?

Question 2 options

3. After a successful apply, where can the plaintext password be read? Choose the complete answer.

Question 3 options

4. Why does Terraform refuse to use a sensitive value as a for_each key?

Question 4 options

Flashcards

Question

What does sensitive = true guarantee, and what does it leave untouched?

Answer

Guaranteed: the Terraform CLI masks the value as (sensitive value) in plan, apply, console, and diagnostics, and propagates the mark to anything derived from it. Untouched: state files, saved plan binaries, terraform show -json and terraform output -json (which carry plaintext plus a sensitive flag), provider debug logs, and the value sent to the provider API.

Question

How does sensitivity propagate through a configuration?

Answer

Since Terraform 0.14 the mark is viral: any expression computed from a sensitive value becomes sensitive — concatenations, lookups, splats over sensitive lists, derived attributes. An output that touches a sensitive value anywhere must set sensitive = true or Terraform fails with Output refers to sensitive values.

Question

Where may a sensitive value NOT be used, and what error proves it?

Answer

As a count or for_each key. Keys become resource addresses, appear in diagnostics, and are stored in state — surfaces the CLI never redacts — so Terraform stops with: Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments.

Question

What do ephemeral values (Terraform 1.10) add over sensitive = true?

Answer

sensitive only masks display; ephemeral values are never persisted to plan or state at all. Ephemeral variables and ephemeral resources live for a single operation, are re-read each phase, and may only be used in ephemeral-capable contexts — using one in an ordinary stored resource argument is a hard error.

Question

How do write-only arguments (Terraform 1.11) work, and what drives their updates?

Answer

Arguments like aws_db_instance.password_wo accept a value the provider uses during the operation; Terraform never stores it in plan or state. Because the value cannot be diffed, the paired *_wo_version argument (which is stored in state) drives updates: increment the version so the provider fetches and applies the new value.

Revision Notes

Key Takeaways

  • 1. sensitive = true protects only CLI display surfaces; state, saved plans, and JSON formats carry the plaintext by design
  • 2. Sensitivity is viral — derived values inherit it — and Terraform enforces that with hard errors (Output refers to sensitive values, forbidden for_each keys)
  • 3. State is the real secret store in most Terraform setups; encrypt it, lock it, and treat every reader as a secret holder
  • 4. Terraform 1.10 ephemeral values and 1.11 write-only arguments keep secrets out of plan and state entirely, with _wo_version arguments driving rotation
  • 5. Machine-readable output (show -json, output -json) deliberately includes real values plus sensitive metadata — consumers must mask before sharing

Interview Tips

  • • "What does sensitive = true actually do?" — answer with the boundary: it redacts CLI rendering and propagates the mark; then immediately name where plaintext still lives. Interviewers probe whether you confused redaction with encryption
  • • "How do you keep a password out of Terraform state?" — ephemeral variable or ephemeral resource feeding a write-only argument like password_wo, with password_wo_version to trigger rotation; mention the 1.10/1.11 version requirements
  • • "A secret leaked through CI. Terraform printed it masked. How?" — walk the leak paths: plan JSON in an artifact, tf.plan binary archived, TF_LOG trace, or raw state access; the CLI was never the weak link
  • • "Why can't a sensitive value be a for_each key?" — keys land in addresses, diagnostics, and state, none of which redact; blocking up front prevents the leak instead of discovering it later

Cheat Sheet

Sensitive Values and Redaction

What the flag does: masks the value as (sensitive value) in plan, apply, console, diagnostics; propagates to derived values; blocks use as count/for_each keys.

Where plaintext still lives:

Artifact Plaintext? Why
plan/apply/console text No CLI redacts
terraform output -json Yes machine consumers get value + flag
terraform.tfstate / remote state Yes Terraform must diff prior vs proposed
saved tf.plan binary Yes apply runs without re-reading variables
terraform show -json Yes after_sensitive is metadata for tools
TF_LOG provider traces Yes RPC request bodies are logged verbatim

State-safe secret flow (Terraform 1.11+):

ephemeral "random_password" "db" {
  length = 20
}

resource "aws_db_instance" "orders" {
  password_wo         = ephemeral.random_password.db.result
  password_wo_version = 1   # stored in state; bump to rotate
}

Rules: sensitive is redaction, not encryption. Ephemeral values are never persisted. Write-only _wo arguments accept values Terraform discards after the operation; the _wo_version sibling triggers updates. nonsensitive() strips the mark — needs a written reason in review.

Continue Learning