Terraform · lesson 11 of 17
Beginner
7- 1 Terraform Fundamentals (IaC)
- 2 Terraform Workflow (init → validate → plan → apply)
- 3 Terraform State (tfstate, remote state, locking)
- 4 Terraform Variables and Outputs
- 5 Providers and Resources (the building blocks of Terraform)
- 6 Terraform Locals: Computed Values Without Repetition
- 7 Terraform Modules (reuse, composition, and clean structure)
Intermediate
7- 8 Terraform count and for_each (repeat resources safely)
- 9 The lifecycle Block: prevent_destroy, create_before_destroy, ignore_changes
- 10 Data Sources and Dependencies (read existing infrastructure)
- 11 sensitive = true: What It Hides, and What It Doesn't
- 12 Remote State Safety (locking, drift, and recovery)
- 13 Workspaces and Environments (dev/stage/prod patterns)
- 14 Terraform CI/CD with GitHub Actions (plan/apply)
Advanced
3Also part of Terraform
4Also 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 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.