Manage Secrets and Sensitive Data in Terraform

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/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:

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

NOTE
Use the Terraform lab environment on Ubuntu. Run terraform init in each subdirectory before your first plan. Examples use the built-in 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:

  • .tf configuration and variable assignment files
  • shell environment and process listings
  • plan output and saved plan files
  • state and remote backend storage
  • explicit terraform output queries (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:

hcl
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:

bash
mkdir -p ~/terraform-labs/terraform-sensitive-data/sensitive-var

Save the HCL above as main.tf in that directory, then initialize providers:

bash
cd ~/terraform-labs/terraform-sensitive-data/sensitive-var && terraform init -input=false

Plan the stack to see how Terraform prints the resource argument:

bash
terraform plan -input=false -no-color

Sample output:

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:

bash
mkdir -p ~/terraform-labs/terraform-sensitive-data/plain-var && cd ~/terraform-labs/terraform-sensitive-data/plain-var

Write main.tf with a plain default and no sensitive flag:

bash
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
}
EOF

Initialize providers in the plain-var directory:

bash
terraform init -input=false

Plan without the sensitive marker so the literal value appears in output:

bash
terraform plan -input=false -no-color

Sample output:

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:

bash
cd ~/terraform-labs/terraform-sensitive-data/sensitive-var && terraform apply -auto-approve -input=false -no-color

Apply 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:

bash
terraform state show terraform_data.demo

Sample output:

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:

bash
grep 'fake-token-lab-only' terraform.tfstate

Sample output:

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:

hcl
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:

bash
mkdir -p ~/terraform-labs/terraform-sensitive-data/sensitive-output

Write main.tf with the blocks above, initialize, and apply:

bash
cd ~/terraform-labs/terraform-sensitive-data/sensitive-output && terraform init -input=false

Apply so the sensitive output is written into state:

bash
terraform apply -auto-approve -input=false -no-color

A default terraform output listing redacts sensitive values:

bash
terraform output

Sample output:

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:

bash
terraform output api_token

Sample output:

output
"fake-token-lab-only"

The same disclosure paths apply with explicit flags:

bash
terraform output -raw api_token

Sample output:

output
fake-token-lab-only

When you pass an output name to -json, Terraform returns only that output's JSON value — not the metadata wrapper:

bash
terraform output -json api_token

Sample output:

output
"fake-token-lab-only"

To see the sensitive, type, and value metadata object, query the full outputs collection:

bash
terraform output -json

Sample output:

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:

hcl
variable "api_token" {
  type      = string
  sensitive = true
}

resource "terraform_data" "demo" {
  input = var.api_token
}

Create the env-var lab directory and configuration:

bash
mkdir -p ~/terraform-labs/terraform-sensitive-data/env-var

After saving main.tf and running terraform init, export a fake token for this session only:

bash
export TF_VAR_api_token='env-fake-secret-lab'

Plan with the environment supplying the value:

bash
cd ~/terraform-labs/terraform-sensitive-data/env-var && terraform plan -input=false -no-color

Sample output:

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:

bash
unset TF_VAR_api_token

tfvars 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:

hcl
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 = true on variables
  • ephemeral blocks for provider resources that support ephemeral mode (for example ephemeral "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:

hcl
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:

bash
mkdir -p ~/terraform-labs/terraform-sensitive-data/ephemeral-check

After terraform init, apply the configuration:

bash
cd ~/terraform-labs/terraform-sensitive-data/ephemeral-check && terraform apply -auto-approve -input=false -no-color

Sample output:

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 0s

The value was consumed during the operation. Confirm it never landed in state:

bash
grep 'ephemeral-fake-token' terraform.tfstate || echo 'ephemeral token not found in state'

Sample output:

output
ephemeral token not found in state

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

hcl
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:

bash
cd ~/terraform-labs/terraform-sensitive-data/ephemeral-restriction && terraform init -input=false

Run validate to surface the write-only error without applying:

bash
terraform validate -no-color

Sample output:

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:

hcl
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:

bash
cd ~/terraform-labs/terraform-sensitive-data/ephemeral-random-only && terraform init -input=false

Apply so Terraform opens the ephemeral password resource and runs the provisioner:

bash
terraform apply -auto-approve -input=false -no-color

Sample output:

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:

bash
terraform state list

Sample output:

output
terraform_data.marker

If 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):

hcl
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:

text
Terraform configuration
Vault provider data source or resource
Vault API
secret value returned to Terraform evaluation

The 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):

bash
vault server -dev

Dev 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:

bash
cd ~/terraform-labs/terraform-sensitive-data/sensitive-var && terraform plan -input=false -out=sensitive.tfplan -no-color

Inspect the JSON representation:

bash
terraform show -json sensitive.tfplan | grep -o 'fake-token-lab-only' | head -1

Sample output:

output
fake-token-lab-only

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


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.


Frequently Asked Questions

1. Does sensitive true encrypt Terraform state?

No. sensitive redacts values in many CLI views such as plan summaries and terraform state show, but the value can still appear in terraform.tfstate, saved plan files, and explicit terraform output queries by name, -raw, or -json. Protect state files and remote backends with access control and encryption.

2. What is the difference between sensitive and ephemeral in Terraform?

sensitive controls disclosure and redaction in selected Terraform UI and CLI output. ephemeral marks values that Terraform omits from persisted state and plan files when used in supported contexts. They solve different problems and can be combined where the language allows.

3. Can I use an ephemeral variable in any resource argument?

No. Ephemeral values may only flow into supported ephemeral contexts such as locals, other ephemeral variables, child-module ephemeral outputs, write-only arguments, ephemeral blocks, provider configuration, and provisioner or connection blocks. Passing an ephemeral value into a normal persisted attribute fails validation.

4. Does fetching a secret from Vault keep it out of Terraform state?

Not automatically. Whether a Vault-read value reaches state depends on how you wire it through resources, outputs, and provider schemas. Vault stores secrets centrally, but Terraform still persists values assigned to normal resource attributes unless ephemeral or write-only mechanisms apply.

5. Why does terraform output show my sensitive output value when I query by name?

sensitive on an output redacts the default listing during plan and apply, but terraform output with an output name, -raw, or -json are intentional read paths that return the real value. Automation must treat named output queries and state as sensitive surfaces.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)