Sensitive Data in Terraform
Handle sensitive data with variables in Terraform — learn to mark variables as sensitive, protect secrets in state and logs, and apply best practices for secure infrastructure-as-code.
Focus: handle sensitive data with variables
You've committed your main.tf to Git, pushed it to your team's repo, and only later realized that your AWS access key is sitting there in plain text for anyone with read access to see. Or worse — you ran terraform plan in a CI log and the output showed your database password in full. If you're managing infrastructure as code, sensitive data will leak through variables, state files, and logs unless you explicitly protect it. This lesson shows you exactly how to handle sensitive data with variables in Terraform, so your secrets stay secret while your code stays shareable.
The Problem This Lesson Solves
Terraform treats variables as plain text unless you tell it otherwise. When you define a variable like db_password without any protection, Terraform will:
- Print its value in
terraform planandterraform applyoutput when you reference it. - Store it in plain text in your
terraform.tfstatefile, which may be committed or synced to remote state backends. - Expose it in logs generated by CI/CD pipelines or your local terminal.
Even if you're careful about which files you commit, state files capture every resource attribute, including secrets. This is one of the most common ways credentials leak in real-world infrastructure pipelines. For example, a developer might use a variable for an API token, run terraform apply, and the next day the token appears in a screenshot shared in a Slack channel.
Pro tip: Never rely on "just don't look at the output." You need explicit mechanisms to protect sensitive data throughout the entire Terraform lifecycle — from definition to state to plan output.
Additionally, many beginners hardcode secrets directly in variables.tf defaults or in resource blocks, which is even worse because it's both visible and version-controlled. The solution is not to avoid variables, but to use them with sensitivity controls built in.
Core Concept / Mental Model
Think of Terraform variables as labeled boxes. By default, the contents are visible to anyone who opens the box (i.e., views the plan or state). The sensitive attribute is like a lock on the box — it doesn't hide the box, but it ensures the contents are never printed in logs or plan output.
But there's a second layer: even if the variable is locked in the plan, the state file is like a separate ledger that records everything, including secrets. To protect data in state, you must use encryption and access controls on the remote backend.
Here's a mental model in three layers:
- Variable definition — Type, description, and
sensitive = trueflag. - Plan/apply output — The CLI, which respects the
sensitiveflag and redacts values. - State file — The persistent record, which stores raw values unless you encrypt it.
A common misunderstanding is that marking a variable as sensitive alone is enough. In reality, you also need to secure the state backend and avoid writing secrets to files in plain text.
How It Works Step by Step
To handle sensitive data with variables, you follow this sequence:
- Define the variable with
sensitive = truein your variables block. - Pass the value via environment variables, CLI flags, or a secrets tool (not in the code).
- Reference the variable in resources as normal — the sensitive flag only affects CLI output, not usage.
- Secure the state backend — use an encrypted backend like S3 with server-side encryption (SSE) or a managed service like Terraform Cloud.
- Verify that your plan output redacts the value and that your state file doesn't leak it in logs.
Let's break down each step.
Step 1: Mark Variables as Sensitive
In variables.tf, add sensitive = true to any variable that holds secrets:
variable "db_password" {
description = "Password for the application database"
type = string
sensitive = true
}
variable "api_key" {
description = "API key for third-party service"
type = string
sensitive = true
}
This tells Terraform to redact the value in plan/apply output. In Terraform 0.14 and later, you'll see (sensitive value) instead of the actual string.
Step 2: Supply Values Securely
The best way to provide values is via environment variables, which Terraform automatically picks up if they follow the TF_VAR_ naming convention:
export TF_VAR_db_password='SuperSecret123!'
export TF_VAR_api_key='sk-abc123xyz'
terraform plan
You can also use a CLI flag: terraform plan -var 'db_password=SuperSecret123!' — but be aware this may show up in your shell history. For production, use a secrets manager like Vault, AWS Secrets Manager, or an external data source.
Step 3: Reference in Resources
You reference sensitive variables exactly like any other variable — the sensitive flag doesn't change how you use them:
resource "aws_db_instance" "app_db" {
identifier = "app-db"
engine = "postgres"
username = "admin"
password = var.db_password
# ...
}
Step 4: Secure the State Backend
State files store everything in plain text. If you use a local backend, you must encrypt the file yourself and restrict access. For teams, use a remote backend with encryption and access controls. For example, with S3:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Pro tip: Enable S3 bucket versioning and add a bucket policy that denies public access. If a secret leaks into state, you can roll back to previous versions — but you should also rotate the secret immediately.
Step 5: Verify Redaction
After marking variables as sensitive, run terraform plan. You should see (sensitive value) in the output. If you see the actual value, you likely forgot to set sensitive = true or you're using a data source that doesn't support it.
Hands-On Walkthrough
Let's put this into practice. Create a directory with three files: main.tf, variables.tf, and outputs.tf. We'll simulate a simple scenario with a dummy provider (like the random provider) to avoid real cloud costs.
1. Define Variables
variables.tf
variable "db_password" {
description = "Password for the database"
type = string
sensitive = true
}
variable "api_key" {
description = "API key for a service"
type = string
sensitive = true
}
2. Use the Variables
main.tf
resource "random_password" "generated" {
length = 16
special = true
}
resource "local_file" "example" {
content = "db_password: ${var.db_password}\napi_key: ${var.api_key}"
filename = "example.txt"
}
# This output is intentionally marked sensitive
output "db_password" {
value = var.db_password
sensitive = true
}
outputs.tf
output "all_secrets" {
value = {
db = var.db_password
api = var.api_key
}
sensitive = true
}
3. Set Environment Variables and Run
export TF_VAR_db_password='P@ssw0rd!'
export TF_VAR_api_key='sk-secret'
terraform init
export TF_LOG=TRACE # optional, to see the redaction in action
export TF_LOG_PATH=trace.log
export TF_LOG=
terraform apply -auto-approve
Expected Output (excerpt):
# local_file.example will be created
+ resource "local_file" "example" {
+ content = (sensitive value)
+ filename = "example.txt"
}
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
db_password = (sensitive value)
all_secrets = (sensitive value)
Notice that the content and outputs show (sensitive value) instead of the actual secrets.
4. Check the State File
Now look at terraform.tfstate — you'll see the raw values in plain text. This is expected, but it's why you must secure the backend.
cat terraform.tfstate | grep -E 'content|db_password|api_key'
You'll see the actual values. This demonstrates the need for encryption at rest.
Compare Options / When to Choose What
When handling sensitive data, you have several approaches. Here's a comparison:
| Approach | Pros | Cons | Best For |
|---|---|---|---|
| Environment variables | Simple, easy to use, no extra tooling | Can be seen in process lists, not suitable for complex secret rotation | Local development, small projects |
| Terraform Cloud/Enterprise variables | Centralized, versioned, encrypted at rest | Requires paid plan or enterprise setup | Teams with shared workspaces |
| External secrets manager (Vault, AWS Secrets Manager) | Full secret lifecycle, audit logs, rotation, least-privilege access | Requires extra infrastructure and code (data sources) | Production environments, compliance-driven orgs |
random provider |
Generates secrets on the fly, no manual input | Secrets are still in state, requires careful handling | Non-production, transient resources |
When to Choose What
- Choose environment variables for local development and quick tests — you avoid committing secrets.
- Choose a secrets manager for production — you get rotation, IAM, and centralized control. You can fetch secrets with a data source:
data "aws_secretsmanager_secret_version" "db" {
secret_id = "prod-db-password"
}
resource "aws_db_instance" "app" {
password = data.aws_secretsmanager_secret_version.db.secret_string
}
- Choose Terraform Cloud variables if your team is already using it — it's the easiest way to share variable values without exposing them in repos.
Troubleshooting & Edge Cases
Mistake: Marking Variables Sensitive But State Still Leaks
Problem: You set sensitive = true, but the state file still contains the raw secret.
Alternate fix: Remember that sensitive only affects CLI output. You must encrypt the state backend (e.g., S3 SSE, Terraform Cloud) to protect at rest. Also, rotate secrets if they may have been exposed.
Mistake: Outputs Show Sensitive Values Despite sensitive = true
Problem: An output declared as sensitive appears in plain text in the CLI.
Alternate fix: In Terraform 0.12 and earlier, outputs didn't have a sensitive flag. Always use sensitive = true in output blocks. If you're using a module, ensure the module's outputs are also marked sensitive.
Mistake: Passing Variables on the Command Line
Problem: You use -var 'token=secret' and it shows up in shell history or CI logs.
Alternate fix: Use environment variables (TF_VAR_token) or a secrets manager; avoid CLI flags for anything sensitive.
Mistake: Using the random Provider for Production Passwords
Problem: Generated passwords are stored in state and may be lost or exposed.
Alternate fix: For production, use a secrets manager to generate or store credentials. The random provider is fine for non-production, but treat its output as sensitive and secure the state.
Edge Case: Sensitive Variables in for_each or String Interpolation
Problem: You reference a sensitive variable in a string that isn't marked sensitive, causing a warning or leakage.
Alternate fix: Always use the sensitive true flag on outputs and be careful with join and format — if you combine a sensitive value with a non-sensitive string, the result may lose sensitivity. Use sensitive() in outputs where needed.
What You Learned & What's Next
You've learned how to handle sensitive data with variables in Terraform: you can mark variables as sensitive to redact them in plan/apply output, supply values via environment variables or secrets managers instead of hardcoding, protect the state backend with encryption, and verify redaction in output. You also explored alternatives and troubleshooting for common mistakes.
You can now:
- Define sensitive variables with sensitive = true.
- Supply values securely via TF_VAR_* or external secrets.
- Secure the state backend to protect at-rest data.
- Debug common leaks and misconfigurations.
What's next: In the next lesson, you'll learn about remote state locking and consistency — how to prevent concurrent modifications and keep your team's infrastructure in sync. You'll apply these secrets-handling techniques in a collaboration context.
Keep your secrets out of code and logs, and your infrastructure will be safe as you scale.
Practice recap
Create a new empty directory, define three variables (db_user, db_password, api_token) with sensitive = true, and use them in a local file resource. Run terraform plan with TF_VAR_ environment variables and confirm the output shows (sensitive value) for the file content and outputs. Then inspect the state file to see the raw values — this reinforces why you must secure the backend. Next, try using a data source to pull a secret from your cloud provider's secrets manager.
Common mistakes
- Marking a variable as sensitive but still storing plaintext in state — sensitive only hides CLI output, not state.
- Using
-var 'secret=value'on the command line, which exposes the secret to shell history and process listings. - Hardcoding secrets as default values in variables.tf or inside resource blocks — always visible in the repo.
- Forgetting to mark outputs as sensitive, so values reappear in console output even when the variable is protected.
- Reliance on the
randomprovider for production credentials without securing the state backend.
Variations
- Use a secrets manager like Vault or AWS Secrets Manager to fetch secrets via data sources instead of inputting them manually.
- Use Terraform Cloud or Enterprise variables to store secrets encrypted at rest and manage them per workspace.
- Use the
terraform.tfvarsfile (with prevent access via permissions and outside VCS) instead of environment variables — but remember it still holds plaintext.
Real-world use cases
- Automated CI/CD pipelines that provision AWS resources using API keys injected as TF_VAR_ env vars without leaking plan logs.
- A team managing a multi-environment deployment where database passwords are stored in AWS Secrets Manager and fetched during apply.
- An organization with audit requirements that protects state files in an encrypted S3 bucket and rotates secrets automatically.
Key takeaways
- Mark any variable holding credentials, tokens, or passwords with
sensitive = truein its variable block. - Never put secrets in code or VCS; supply values via environment variables, CLI flags, or secrets managers.
- The
sensitiveflag only redacts CLI output — state files store plaintext, so always encrypt and restrict access to backend storage. - Thoroughly test with
terraform planto ensure you see(sensitive value)instead of the actual secret. - Use a secrets manager for production to enable rotation, auditing, and least-privilege access.
- When combining sensitive values with non-sensitive ones in expressions, verify the output doesn't become non-sensitive unexpectedly.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.