| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1hashicorp/random 3.9.0 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user; sudo only if Terraform is not installed yet |
| Scope | Terraform sensitive variables and outputs, CLI redaction limits, state and saved-plan disclosure, TF_VAR and tfvars guidance, ephemeral variables and resources, write-only argument concepts, Vault provider overview, comparison tables, and common mistakes. Does not cover HCP Terraform variable sets, cloud secret-manager tutorials, Vault cluster administration, secret rotation architecture, or production IAM design. |
| Related guides | Terraform variables Terraform output values Terraform state terraform plan command Terraform Associate certification course |
Passwords, API tokens, and private keys do not belong in casual terminal scrollback or committed Git history. Terraform gives you several overlapping mechanisms to limit accidental disclosure — but they are not interchangeable.
Four ideas show up together on modern Terraform releases. Keep them separate before you pick one:
sensitive → controls disclosure/redaction in selected Terraform CLI output
ephemeral → omits eligible values from persisted state/plan where supported
write-only argument → provider exposes an argument that is set but not stored normally
external secret mgr → stores and retrieves secrets outside Terraform (Vault, etc.)These are modern Terraform mechanisms relevant to safely handling sensitive data under Associate objective 4h. The labs below use fake disposable secrets only (fake-token-lab-only, env-fake-secret-lab, ephemeral-fake-token) — never paste real credentials into configuration or shell history.
Each demo uses its own subdirectory under ~/terraform-labs/terraform-sensitive-data/ so plan output from one exercise does not collide with another.
terraform_data resource and hashicorp/random where ephemeral resources are demonstrated — no cloud credentials required.
Why Terraform secrets need special handling
A secret you type once can still appear in many places Terraform touches during a normal workflow:
.tfconfiguration and variable assignment files- shell environment and process listings
- plan output and saved plan files
- state and remote backend storage
- explicit
terraform outputqueries (including-json) - CI logs and shared automation artifacts
sensitive = true helps with casual CLI redaction. It does not magically remove the value from every persistence surface. The sections below show exactly where redaction stops.
Sensitive variables and outputs
Mark variables sensitive
Mark an input variable sensitive when you want Terraform to treat its value as confidential in normal CLI operation:
variable "api_token" {
type = string
default = "fake-token-lab-only"
sensitive = true
}
resource "terraform_data" "demo" {
input = var.api_token
}
output "demo_id" {
value = terraform_data.demo.id
}Create an isolated lab directory and write the configuration:
mkdir -p ~/terraform-labs/terraform-sensitive-data/sensitive-varSave the HCL above as main.tf in that directory, then initialize providers:
cd ~/terraform-labs/terraform-sensitive-data/sensitive-var && terraform init -input=falsePlan the stack to see how Terraform prints the resource argument:
terraform plan -input=false -no-colorSample output:
Terraform will perform the following actions:
# terraform_data.demo will be created
+ resource "terraform_data" "demo" {
+ id = (known after apply)
+ input = (sensitive value)
+ output = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.The plan redacts input even though you set a default in configuration. A non-sensitive variable with the same shape shows the literal during plan — create a separate plain-var directory for the contrast:
mkdir -p ~/terraform-labs/terraform-sensitive-data/plain-var && cd ~/terraform-labs/terraform-sensitive-data/plain-varWrite main.tf with a plain default and no sensitive flag:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "api_token" {
type = string
default = "plain-label-lab"
}
resource "terraform_data" "demo" {
input = var.api_token
}
EOFInitialize providers in the plain-var directory:
terraform init -input=falsePlan without the sensitive marker so the literal value appears in output:
terraform plan -input=false -no-colorSample output:
+ input = "plain-label-lab"That visible string is why sensitive = true exists for credentials you do not want echoed in routine plan output.
Apply the sensitive-variable stack so state exists for the next subsection:
cd ~/terraform-labs/terraform-sensitive-data/sensitive-var && terraform apply -auto-approve -input=false -no-colorApply completes with no secret printed in the summary when outputs are not exposing the token directly.
What sensitive = true actually does
sensitive is a disclosure control, not encryption. Terraform still needs the real value to manage resources, and many persistence surfaces store it in cleartext.
After apply, ask terraform state show what it will print to the terminal:
terraform state show terraform_data.demoSample output:
# terraform_data.demo:
resource "terraform_data" "demo" {
id = "2f1b41f6-9d6a-3620-f00a-374c7576e14a"
input = (sensitive value)
}CLI redaction hides the token in this view. That is not the same as omitting it from the state file.
Search the on-disk state JSON for the fake lab token:
grep 'fake-token-lab-only' terraform.tfstateSample output:
"value": "fake-token-lab-only",The sensitive marker affects many CLI presentations, but terraform.tfstate still contains the literal value. Treat every copy of state — local files, remote backends, and backups — as a secret-bearing artifact. The Terraform state lesson covers protection and Git rules; this article focuses on what sensitive does and does not do.
Sensitive outputs and explicit read paths
Outputs can be sensitive independently of the variables that feed them:
variable "api_token" {
type = string
default = "fake-token-lab-only"
sensitive = true
}
output "api_token" {
value = var.api_token
sensitive = true
}Use a fresh directory so output behavior is isolated:
mkdir -p ~/terraform-labs/terraform-sensitive-data/sensitive-outputWrite main.tf with the blocks above, initialize, and apply:
cd ~/terraform-labs/terraform-sensitive-data/sensitive-output && terraform init -input=falseApply so the sensitive output is written into state:
terraform apply -auto-approve -input=false -no-colorA default terraform output listing redacts sensitive values:
terraform outputSample output:
api_token = <sensitive>That redaction applies to the summary listing only. Query the output by name and Terraform prints the real value — even without -raw or -json:
terraform output api_tokenSample output:
"fake-token-lab-only"The same disclosure paths apply with explicit flags:
terraform output -raw api_tokenSample output:
fake-token-lab-onlyWhen you pass an output name to -json, Terraform returns only that output's JSON value — not the metadata wrapper:
terraform output -json api_tokenSample output:
"fake-token-lab-only"To see the sensitive, type, and value metadata object, query the full outputs collection:
terraform output -jsonSample output:
{
"api_token": {
"sensitive": true,
"type": "string",
"value": "fake-token-lab-only"
}
}| Command | Sensitive output behavior |
|---|---|
terraform output |
Redacted as <sensitive> |
terraform output api_token |
Real value shown (HCL-quoted string) |
terraform output -raw api_token |
Real value shown (unquoted) |
terraform output -json api_token |
Real value shown as JSON string |
terraform output -json |
Metadata object includes plaintext value |
Pipeline jobs that call any named output query must treat stdout, log capture, and state as sensitive surfaces — not only the redacted plan summary. See Terraform output values for CLI flag details.
If you try to export a sensitive value through a non-sensitive output, Terraform blocks the configuration at validation time and asks you to set sensitive = true on the output block.
Supply secrets without hardcoding
TF_VAR environment variables
Environment variables keep literal secrets out of .tf files, but they are not a complete secrets-management strategy on their own. Shell history, process environment listings, and CI variable stores can still expose values.
Declare a required sensitive variable with no default:
variable "api_token" {
type = string
sensitive = true
}
resource "terraform_data" "demo" {
input = var.api_token
}Create the env-var lab directory and configuration:
mkdir -p ~/terraform-labs/terraform-sensitive-data/env-varAfter saving main.tf and running terraform init, export a fake token for this session only:
export TF_VAR_api_token='env-fake-secret-lab'Plan with the environment supplying the value:
cd ~/terraform-labs/terraform-sensitive-data/env-var && terraform plan -input=false -no-colorSample output:
+ input = (sensitive value)
Plan: 1 to add, 0 to change, 0 to destroy.Terraform maps TF_VAR_<name> to var.<name> as described in Terraform variables. Clear the variable when you finish so your shell does not keep the fake secret:
unset TF_VAR_api_tokentfvars and secrets
Terraform does not mark a terraform.tfvars file as sensitive. A *.tfvars file is plain HCL on disk like any other assignment file.
If you keep secret values in secrets.auto.tfvars locally:
- add that filename to
.gitignore - restrict filesystem permissions
- never commit secret tfvars to Git or paste them into tickets
auto.tfvars loading order and precedence are covered in Terraform variables. This article only stresses that sensitivity metadata lives on variable blocks, not on filenames.
Why secrets should not be hardcoded
This pattern is easy to write and painful to unwind:
resource "terraform_data" "bad_example" {
input = "SuperSecret123"
}Source control history keeps literals even after you delete the line. Prefer variables, environment injection, or a secret manager — and mark sensitive inputs sensitive = true when they pass through Terraform's CLI surfaces.
Ephemeral values in Terraform
Ephemeral values are designed for data that should not be written into persisted plan or state artifacts when Terraform handles them through supported paths. On Terraform 1.15.8 that includes:
ephemeral = trueon variablesephemeralblocks for provider resources that support ephemeral mode (for exampleephemeral "random_password")
Ephemeral values are available only in contexts the language allows — locals, other ephemeral variables, child-module ephemeral outputs, managed-resource write-only arguments, ephemeral blocks, provider configuration, and provisioner or connection blocks. They are not a drop-in replacement for every string argument.
Ephemeral variables
Declare an ephemeral variable when the value itself should not be persisted. Pass it through a supported ephemeral context such as a local-exec provisioner environment map — not through a check block, which is not an allowed ephemeral consumer:
variable "session_token" {
type = string
ephemeral = true
default = "ephemeral-fake-token"
}
resource "terraform_data" "marker" {
input = "ok"
provisioner "local-exec" {
environment = {
SESSION_TOKEN = var.session_token
}
command = "test -n \"$SESSION_TOKEN\""
}
}The provisioner runs during apply and receives the token in its environment. Terraform suppresses provisioner log output when ephemeral values are involved.
Create the ephemeral-check lab:
mkdir -p ~/terraform-labs/terraform-sensitive-data/ephemeral-checkAfter terraform init, apply the configuration:
cd ~/terraform-labs/terraform-sensitive-data/ephemeral-check && terraform apply -auto-approve -input=false -no-colorSample output:
terraform_data.marker: Provisioning with 'local-exec'...
terraform_data.marker (local-exec): (output suppressed due to ephemeral value in config)
terraform_data.marker: Creation complete after 0sThe value was consumed during the operation. Confirm it never landed in state:
grep 'ephemeral-fake-token' terraform.tfstate || echo 'ephemeral token not found in state'Sample output:
ephemeral token not found in stateThat is the behavior you want for transient credentials used only during apply — not stored beside resource IDs.
Restrictions on ephemeral propagation
Ephemeral values cannot flow into normal persisted resource arguments. Terraform validates that at terraform validate time.
variable "session_token" {
type = string
ephemeral = true
default = "ephemeral-fake-token"
}
resource "terraform_data" "demo" {
input = var.session_token
}Save that configuration under ~/terraform-labs/terraform-sensitive-data/ephemeral-restriction/ and validate:
cd ~/terraform-labs/terraform-sensitive-data/ephemeral-restriction && terraform init -input=falseRun validate to surface the write-only error without applying:
terraform validate -no-colorSample output:
Error: Invalid use of ephemeral value
with terraform_data.demo,
on main.tf line 13, in resource "terraform_data" "demo":
13: input = var.session_token
Ephemeral values are not valid for "input", because it is not a write-only
attribute and must be persisted to state.The error names the escape hatch: persisted attributes need either a normal (non-ephemeral) value or a provider-defined write-only argument that opts out of normal state storage.
Ephemeral resources
Provider-maintained ephemeral resources generate values that Terraform does not store in plan or state when you consume them only through supported ephemeral contexts:
terraform {
required_version = ">= 1.12.0"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
ephemeral "random_password" "pw" {
length = 16
special = false
}
resource "terraform_data" "marker" {
input = "applied"
provisioner "local-exec" {
environment = {
GENERATED_PASSWORD = ephemeral.random_password.pw.result
}
command = "test -n \"$GENERATED_PASSWORD\""
}
}ephemeral.random_password.pw.result flows into the provisioner environment — an explicitly supported path — without assigning the generated password to a persisted resource attribute.
Create ~/terraform-labs/terraform-sensitive-data/ephemeral-random-only/, initialize with the random provider, and apply:
cd ~/terraform-labs/terraform-sensitive-data/ephemeral-random-only && terraform init -input=falseApply so Terraform opens the ephemeral password resource and runs the provisioner:
terraform apply -auto-approve -input=false -no-colorSample output:
ephemeral.random_password.pw: Opening...
ephemeral.random_password.pw: Opening complete after 0s
terraform_data.marker: Provisioning with 'local-exec'...
terraform_data.marker (local-exec): (output suppressed due to ephemeral value in config)
ephemeral.random_password.pw: Closing...Confirm the ephemeral resource itself is absent from state — only the marker resource is tracked:
terraform state listSample output:
terraform_data.markerIf you wire ephemeral.random_password.pw.result into terraform_data.input, you get the same write-only validation error as with ephemeral variables — the generated secret would have to be persisted.
Ephemeral outputs
Child modules may declare ephemeral = true on outputs so callers inside ephemeral-aware composition can pass values without persisting them at the module boundary. Root modules on Terraform 1.15.8 cannot export ephemeral outputs — validation fails with Ephemeral output not allowed in the root module context.
For Associate study, remember: ephemeral outputs are a module-composition tool, not something you declare in a root module output block the way you expose demo_id above.
Write-only arguments
Provider-defined write-only arguments
A write-only argument is declared by the provider in its resource schema. You cannot add write_only = true to arbitrary arguments yourself. Write-only arguments require Terraform 1.11+ and explicit provider/resource support.
Typical pattern on providers that support it (AWS RDS example from provider documentation — not executed in this lab):
resource "aws_db_instance" "example" {
identifier = "tf-sensitive-data-demo"
engine = "postgres"
instance_class = "db.t3.micro"
password_wo = var.db_password
password_wo_version = 1
}Terraform passes the write-only value to the provider during the operation but does not store that argument in plan or state. What the remote service does with the supplied secret is provider/resource-specific. The exact argument names differ per resource type — consult the provider docs for the service you manage.
This course lab proves the contract with the ephemeral restriction error: only write-only (or otherwise non-persisted) arguments may receive ephemeral values. Without a supported write-only argument on your target resource, you must keep secrets in normal persisted attributes — which means state can contain them unless you redesign the flow.
Sensitive vs ephemeral vs write-only
| Mechanism | Primary purpose | CLI redaction | Persisted in state | Persisted in saved plan | Provider support required | Typical use |
|---|---|---|---|---|---|---|
sensitive = true (variable/output) |
Limit casual CLI disclosure | Yes — many plan/apply views | Often yes — value can still be in terraform.tfstate |
Often yes — check JSON plan exports | No — core Terraform | Tokens referenced by resources that must be stored |
ephemeral = true (variable) |
Omit eligible values from persisted artifacts | N/A — value may not appear in persisted views | No — when used only in supported ephemeral contexts | No — when not assigned to persisted attrs | No — core Terraform | Short-lived values passed to provisioners or write-only args |
ephemeral resource block |
Generate or read transient provider data | N/A | No — when result stays in ephemeral contexts | No — when not wired to persisted attrs | Yes — per resource type | Generated passwords consumed during apply |
| Write-only argument | Set provider secret without Terraform artifact storage | Provider-dependent | No | No | Yes — per argument | Database passwords, API keys on supported cloud resources |
| External secret manager (Vault, etc.) | Central storage and rotation | Terraform still redacts only where you mark sensitive |
Depends on how values flow through resources | Depends on wiring | Provider + external system | Organization-wide secret storage |
These mechanisms combine in real stacks — for example a Vault-read value passed to a write-only argument with sensitive outputs redacted for operators.
Terraform and external secret managers
Vault overview
HashiCorp Vault stores secrets outside Terraform configuration. A typical flow:
Terraform configuration
↓
Vault provider data source or resource
↓
Vault API
↓
secret value returned to Terraform evaluationThe Vault provider can read dynamic credentials at plan/apply time so literals never sit in .tf files. This article does not install or administer Vault — that is a separate operations track. For exam context, know that Terraform integrates with Vault through the provider; you still must reason about where the retrieved value flows afterward.
Optional disposable dev mode (do not use in production):
vault server -devDev mode starts a local listener with a known root token printed once at startup. Use it only on a throwaway machine to explore provider data sources — not as a pattern for production secret storage.
Does Vault keep secrets out of Terraform state?
No — not automatically. Vault is the system of record for storage and rotation. Terraform still evaluates provider results into resource arguments and outputs you define.
If a Vault-read password is assigned to a normal persisted resource attribute or a non-sensitive output, state and plan artifacts can still contain it. Ephemeral variables, ephemeral resources, write-only arguments, and sensitive markings change different parts of that pipeline. Design the data flow explicitly rather than assuming Vault absolves state of secrets.
Other sensitive persistence surfaces
Saved plan files
Saved plans are persistence surfaces. When you pass -out to terraform plan, Terraform writes a plan file that tooling later consumes with terraform apply <file>. HashiCorp documents that saved plans may contain sensitive values in cleartext.
From the sensitive-variable lab directory, save a plan:
cd ~/terraform-labs/terraform-sensitive-data/sensitive-var && terraform plan -input=false -out=sensitive.tfplan -no-colorInspect the JSON representation:
terraform show -json sensitive.tfplan | grep -o 'fake-token-lab-only' | head -1Sample output:
fake-token-lab-onlyTreat *.tfplan files like state — restrict permissions, avoid archiving them in ticket systems, and delete them when the run finishes.
terraform output and JSON
The sensitive-output lab showed the full disclosure hierarchy. Any wrapper script that posts CI output to a log aggregator must treat named terraform output queries the same as -raw and -json — all return the real value for sensitive outputs when queried by name.
Use terraform output without arguments when you want the redacted listing. Use terraform output -json (no name) when tooling needs the metadata wrapper — and scrub the value fields before logging.
Secret-handling decision guide
| If you need… | Start with… |
|---|---|
| Casual CLI redaction during plan/apply | sensitive = true on variables and outputs |
| A value omitted from state/plan when used correctly | ephemeral = true variables or supported ephemeral resources in provisioner or write-only contexts |
| A provider secret set without Terraform artifact storage | A documented write-only argument on that resource type |
| Central storage, leasing, and rotation | Vault or another secret manager — then design Terraform wiring explicitly |
You will often combine rows — sensitive outputs for operators plus Vault for storage plus write-only arguments on the target cloud resource.
Common mistakes and troubleshooting
| Mistake | Why it hurts |
|---|---|
Hardcoding credentials in .tf files |
Git history retains literals forever |
Committing secrets.tfvars |
tfvars files are not magically sensitive |
Assuming sensitive encrypts state |
State and JSON plans can still hold cleartext |
| Assuming Vault prevents state persistence | Retrieved values follow the attributes and outputs you assign |
Running terraform output api_token in logged CI |
Named queries reveal sensitive values without -raw or -json |
| Expecting every argument to support write-only | Only provider-schema write-only arguments qualify |
| Passing ephemeral values into normal resource attrs | Validation fails — by design |
Using ephemeral variables in check blocks |
check is not a supported ephemeral context |
Archiving *.tfplan without access controls |
Saved plans can embed secrets |
| Symptom | Likely cause | Fix |
|---|---|---|
Plan shows (sensitive value) but grep finds the secret in terraform.tfstate |
sensitive redacts CLI views, not all persistence |
Protect state/backends; use ephemeral or write-only paths when omission is required |
Error: Output refers to sensitive values |
Non-sensitive output exposes a sensitive value | Add sensitive = true to the output or stop exporting the secret |
Invalid use of ephemeral value |
Ephemeral value assigned to persisted argument | Route through write-only argument or provisioner context |
Ephemeral output not allowed |
Root module output with ephemeral = true |
Move ephemeral outputs to child modules or export non-ephemeral summaries only |
terraform output shows <sensitive> but terraform output api_token reveals value |
Named output queries are intentional read paths | Treat all named output queries, -raw, -json, and state as sensitive in automation |
| Secret appears in saved plan JSON | Plans persist proposed values | Delete plan files after apply; restrict permissions during CI |
References
- Sensitive variables — Terraform language documentation
- Sensitive values in state — Terraform documentation
- Ephemeral values — Terraform language documentation
- Write-only arguments — Terraform language documentation
- terraform output command — Terraform CLI documentation
- Vault provider documentation
Summary
You walked through fake-secret labs that separate disclosure controls from persistence controls. sensitive = true on variables and outputs redacts many plan and apply views, but terraform.tfstate, saved plan JSON, and explicit output queries — including terraform output api_token by name — can still expose the literal value. State and plan files need the same protection as production credentials.
Ephemeral variables and ephemeral provider resources omit values from persisted artifacts when you route them through supported contexts such as provisioner environment maps or write-only arguments. The validation error for assigning ephemeral data to terraform_data.input is the proof point for write-only parameters: persisted attributes require either normal values or provider-defined write-only arguments.
Vault and other secret managers centralize storage, yet Terraform still persists whatever you assign to normal resource arguments unless you design ephemeral and write-only flows deliberately. Use the comparison and decision tables when you stack mechanisms — redaction, omission, write-only provider args, and external managers solve different layers of the same problem.
When you finish, run terraform destroy in each lab subdirectory you created and remove disposable plan files such as sensitive.tfplan.

