Skip to main content
Terraform · lesson 11 of 17
Terraform intermediate Lesson 11 of 17

Also part of: Cryptography DevSecOps (AppSec in the pipeline) Cloud Security

sensitive = true: What It Hides, and What It Doesn't

sensitive = true masks a secret in plan output, but state keeps it in plain text. Real runs show both, plus why an ephemeral variable is refused.

By SkillByExample · Updated

terraform security sensitive state ephemeral

terraform plan prints every value it’s about to set. That’s the point of reviewing a plan, but for a password it means the plan output gives the secret away. Examples use Terraform 1.16.5 and hashicorp/local 2.9.1.

Without sensitive, the secret prints in full

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.9"
    }
  }
}

variable "db_password" {
  type    = string
  default = "hunter2-actual-secret"
}

resource "local_file" "db_config" {
  filename = "${path.module}/db-config.txt"
  content  = "password=${var.db_password}\n"
}

output "password_used" {
  value = var.db_password
}
$ terraform apply -auto-approve -no-color

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.db_config will be created
  + resource "local_file" "db_config" {
      + content              = <<-EOT
            password=hunter2-actual-secret
        EOT
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "./db-config.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + password_used = "hunter2-actual-secret"
local_file.db_config: Creating...
local_file.db_config: Creation complete after 0s [id=b4cb3bfee18c9844004095c598b9ae0629918c29]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

password_used = "hunter2-actual-secret"

The password appears three times: in the resource diff, in the output diff and in the outputs summary. In CI, all three land in a build log that many people can read.

sensitive = true masks it, and forces a decision

variable "db_password" {
  type      = string
  default   = "hunter2-actual-secret"
  sensitive = true
}
$ terraform plan -no-color

Error: Output refers to sensitive values

  on main.tf line 21:
  21: output "password_used" {

To reduce the risk of accidentally exporting sensitive data that was intended
to be only internal, Terraform requires that any root module output
containing sensitive data be explicitly marked as sensitive, to confirm your
intent.

If you do intend to export this data, annotate the output value as sensitive
by adding the following argument:
    sensitive = true

Marking the variable sensitive isn’t enough by itself. Terraform refuses to plan until every root output that exposes the value says so too. Add sensitive = true to the output:

$ terraform apply -auto-approve -no-color
local_file.db_config: Refreshing state... [id=b4cb3bfee18c9844004095c598b9ae0629918c29]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # local_file.db_config will be updated in-place
  ~ resource "local_file" "db_config" {
      # Warning: this attribute value will be marked as sensitive and will not
      # display in UI output after applying this change. The value is unchanged.
      ~ content              = (sensitive value)
        id                   = "b4cb3bfee18c9844004095c598b9ae0629918c29"
        # (9 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
local_file.db_config: Modifying... [id=b4cb3bfee18c9844004095c598b9ae0629918c29]
local_file.db_config: Modifications complete after 0s [id=b4cb3bfee18c9844004095c598b9ae0629918c29]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Outputs:

password_used = <sensitive>

local_file.db_config never mentions sensitive, but its content is masked, because it interpolates the variable. The “update in-place” changes nothing on disk (“The value is unchanged”). It only records in state that this attribute is now sensitive.

The state file still has the real value

$ grep -A2 hunter2 terraform.tfstate
      "value": "hunter2-actual-secret",
      "type": "string",
      "sensitive": true
--
            "content": "password=hunter2-actual-secret\n",
            "content_base64": null,
            "content_base64sha256": "Pp3A/aTU721OCo1GEqBcs2UzoQA7jg2mcjRAzaNmie8=",

"sensitive": true sits right next to the plaintext. It tells the CLI not to print the value, and that’s all it does. Anyone who can read the state file can read the password: a local file, an S3 object, a CI cache. sensitive protects terminals and logs. Protecting the state file is the backend’s job.

Keeping a secret out of state: ephemeral

Since Terraform 1.10, a variable can be marked ephemeral, which means it’s never written to state or plan files. Here’s what happens when one flows into a normal attribute:

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

resource "local_file" "db_config" {
  filename = "${path.module}/db-config.txt"
  content  = "password=${var.db_password}\n"
}
$ terraform plan -var db_password=hunter2 -no-color

Error: Invalid use of ephemeral value

  with local_file.db_config,
  on main.tf line 17, in resource "local_file" "db_config":
  17:   content  = "password=${var.db_password}\n"

Ephemeral values are not valid for "content", because it is not a write-only
attribute and must be persisted to state.

Terraform refuses instead of quietly storing the secret. Ephemeral values may only go to places that are never persisted. Those are provider configuration blocks and write-only arguments, which some providers offer for secrets, such as password_wo on several cloud database resources. local_file has no write-only argument, so this is a good check on whether a resource can take a secret without storing it.

Practice

1. Build an output that mixes a non-sensitive label with the password. Is all of it masked, or only the password part?

Solution
variable "environment_label" {
  type    = string
  default = "staging"
}

output "combined" {
  value     = "${var.environment_label}: ${var.db_password}"
  sensitive = true
}
$ terraform apply -auto-approve -no-color | tail -4
Outputs:

combined = <sensitive>
password_used = <sensitive>

All of it. Sensitivity applies to the whole resulting value, not to individual characters, so anything a secret touches becomes sensitive.

2. Ask for the masked output directly, by name and as JSON.

Solution
$ terraform output password_used
"hunter2-actual-secret"
$ terraform output -raw password_used
hunter2-actual-secret
$ terraform output -json
{
  "password_used": {
    "sensitive": true,
    "type": "string",
    "value": "hunter2-actual-secret"
  }
}

Only the summary at the end of apply, and plain terraform output, mask the value. Asking for an output by name or as JSON prints it, because scripts need some way to read it. sensitive controls what gets displayed, not who can access the value.

3. Check whether .terraform.lock.hcl also contains the secret.

Solution
$ grep hunter2 .terraform.lock.hcl; echo "exit=$?"
exit=1

No match. The lock file only records provider versions and checksums. It’s safe to commit, while terraform.tfstate never is.

Next: remote state safety, covering locking, drift and recovery.

Frequently Asked Questions

Does sensitive = true encrypt the value in state?
No. It only changes what the CLI prints during plan and apply and in the outputs summary. terraform.tfstate stores the real value in plain JSON either way, with a "sensitive": true marker beside it. Protect state itself with an encrypted, access-controlled backend.
Why does marking a variable sensitive make my output fail?
Sensitivity propagates through every expression that uses the value. A root module output built from a sensitive value fails with 'Output refers to sensitive values' until you add sensitive = true to the output too, so exporting a secret is always a visible, deliberate choice.
Is a resource attribute built from a sensitive variable masked too?
Yes. If a local_file's content interpolates a sensitive variable such as var.db_password, the resource never says sensitive anywhere, but its content attribute still shows as (sensitive value). Terraform tracks sensitivity per value, so the whole string becomes sensitive even if only part of it came from the secret.
Can terraform output reveal a sensitive value?
Yes. terraform output password_used, terraform output -raw and terraform output -json all print the real value, because asking for one output by name is treated as deliberate. Anyone who can run terraform output against the state can read every secret in it.
How do I keep a secret out of state entirely?
Mark the variable ephemeral = true (Terraform 1.10+) and pass it only to write-only arguments, such as password_wo on providers that offer them, or to provider configuration. Terraform refuses to store an ephemeral value in a normal attribute; trying it fails with an 'Invalid use of ephemeral value' error.