Sensitive Variables in Terraform
Terraform’s sensitive attribute prevents secret values from appearing in plan output, apply output, and the CLI. When a value is marked sensitive, Terraform displays (sensitive value) wherever that value would normally be shown — protecting secrets from leaking into logs, CI/CD dashboards, or terminal history.
Marking Variables as Sensitive
variable "database_password" { description = "RDS master password" type = string sensitive = true}
variable "api_key" { description = "Third-party API key" type = string sensitive = true}
variable "tls_private_key" { description = "TLS private key PEM" type = string sensitive = true}
variable "github_token" { description = "GitHub Personal Access Token for CI" type = string sensitive = true}In plan output, sensitive variables appear as:
+ password = (sensitive value) + api_key = (sensitive value)Sensitive Outputs
Any output that contains a sensitive value must itself be marked sensitive:
output "db_connection_string" { description = "Database connection string" value = "postgresql://${var.db_username}:${var.database_password}@${aws_db_instance.main.endpoint}/app" sensitive = true}
output "redis_auth_token" { value = aws_elasticache_replication_group.main.auth_token sensitive = true}If you reference a sensitive variable in an output without marking the output sensitive, Terraform will fail:
Error: Output refers to sensitive values on outputs.tf line 5, in output "connection_string": 5: value = "...${var.database_password}..."Sensitive Locals
When you derive a value from a sensitive variable, the derived local is automatically tainted as sensitive. You can also explicitly mark locals:
locals { db_url = "postgresql://${var.db_user}:${var.database_password}@${aws_db_instance.main.endpoint}/prod" # db_url is automatically sensitive because it contains var.database_password
# Explicitly mark a computed value encoded_credentials = sensitive(base64encode("${var.username}:${var.password}"))}The sensitive() function (Terraform 0.15+) marks any expression as sensitive:
locals { # Mark a computed string sensitive even if no input is sensitive jwt_header = sensitive(base64encode(jsonencode({ alg = "HS256" typ = "JWT" })))}Accessing Sensitive Outputs
Sensitive output values are redacted in the CLI but accessible programmatically:
# Shows "(sensitive value)" in terminalterraform output db_connection_string
# Returns the actual value in JSON formatterraform output -json db_connection_string
# Returns the raw string (for use in scripts)terraform output -raw db_connection_string
# Store in variable without displayingDB_URL=$(terraform output -raw db_connection_string)Sensitive Values Are Still Stored in State
sensitive = true only controls display — it does NOT encrypt the value in state. The actual secret is stored in plaintext in terraform.tfstate.
{ "outputs": { "db_connection_string": { "value": "postgresql://admin:actualpassword@rds.endpoint.com/prod", "type": "string", "sensitive": true } }}Protect the state file with:
# Remote backend with encryptionterraform { backend "s3" { bucket = "mycompany-terraform-state" key = "production/terraform.tfstate" region = "us-east-1" encrypt = true kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/mrk-..." dynamodb_table = "terraform-state-lock" }}Common Anti-Patterns to Avoid
# BAD: Default value contains a secretvariable "db_password" { type = string sensitive = true default = "hardcoded_password_123" # DON'T DO THIS}
# BAD: Sensitive value in a non-sensitive outputoutput "connection_info" { value = { host = aws_db_instance.main.address password = var.database_password # Terraform will error — requires sensitive = true } # Missing: sensitive = true}
# BAD: Logging a sensitive value (it will appear in logs even if marked sensitive)resource "null_resource" "debug" { provisioner "local-exec" { command = "echo ${var.database_password}" # EXPOSED in logs }}Propagation: Sensitive Values Spread
If you pass a sensitive value into a resource attribute, that attribute is automatically treated as sensitive in diffs:
resource "aws_db_instance" "main" { password = var.database_password # Automatically redacted in plan output}
# Any reference to this resource's password attribute is also sensitiveoutput "db_password_again" { value = aws_db_instance.main.password sensitive = true # Required}Non-Sensitive Workaround for Debugging
In development, you may need to inspect a value that’s marked sensitive. Use nonsensitive() temporarily — never in production:
# DEVELOPMENT ONLY — remove before committingoutput "debug_password" { value = nonsensitive(var.database_password) # Displays in plaintext}Remove this before merging. Add a validation in CI:
# Check for nonsensitive() calls before merginggrep -r "nonsensitive(" . --include="*.tf" && echo "ERROR: nonsensitive() found — remove before merging" && exit 1