Sensitive Output #
Marking an output as sensitive = true is the right step — but it’s often over-interpreted. sensitive doesn’t encrypt the value, doesn’t remove it from state, and doesn’t prevent anyone with state access from reading it. All sensitive does is hide the value from the terminal output. Understanding this boundary is important so you don’t get a false sense of security while sensitive values are actually still accessible in various places.
flowchart TD
A["resource db_password"] -->|"output password\n(sensitive = false)"| B["🔓 Shown in the terminal\n& stored in state"]
A -->|"output password\n(sensitive = true)"| C["🔒 Hidden in the terminal\nBut still in state"]
D["remote_state data source"] -->|"Reads the sensitive output"| E["✅ Succeeds\n(no masking)"]
style B fill:#ef4444,stroke:#dc2626,color:#fff
style C fill:#f59e0b,stroke:#d97706,color:#fff
style D fill:#3b82f6,stroke:#1e40af,color:#fff
style E fill:#10b981,stroke:#059669,color:#fffHow to Declare Sensitive Outputs #
# outputs.tf
output "database_password" {
description = "Database password — passed to the deployment pipeline via TF_VAR_*"
value = var.db_password
sensitive = true
}
output "rds_connection_string" {
description = "Full connection string for the production database"
value = "postgresql://${var.db_username}:***@${aws_db_instance.main.endpoint}/${var.db_name}"
sensitive = true
}
output "api_credentials" {
description = "Credentials for an external API"
sensitive = true
value = {
key = var.api_key
secret = var.api_secret
url = aws_apigatewayv2_api.main.api_endpoint
}
}
What sensitive Protects and What It Doesn’t
#
# WHAT IS PROTECTED by sensitive = true:
terraform output
# Outputs:
# database_password = <sensitive> ← hidden in the terminal
# instance_public_ip = "54.123.45.67"
terraform apply
# ... (sensitive outputs aren't shown in the apply log)
terraform plan
# ... (sensitive values don't appear in the diff)
# WHAT IS NOT PROTECTED:
# 1. The value is still stored in plaintext in the state file
cat terraform.tfstate | grep -A5 '"database_password"'
# "value": "super-secret-password-123" ← STILL READABLE
# 2. Still accessible via terraform output -raw
terraform output -raw database_password
# super-secret-password-123 ← SHOWN if accessed explicitly
# 3. Still accessible via terraform output -json
terraform output -json
# { "database_password": { "value": "super-secret-password-123", ... } }
Sensitivity Propagates to Resources #
When a variable or output is marked sensitive, Terraform automatically treats values derived from it as sensitive too.
variable "db_password" {
type = string
sensitive = true
}
resource "aws_db_instance" "main" {
password = var.db_password # The sensitive value "propagates" to this resource
}
# In the plan output, the password attribute on this resource
# will be shown as (sensitive value) instead of its actual value:
#
# ~ resource "aws_db_instance" "main" {
# ~ password = (sensitive value)
# }
Safe Strategies for Sensitive Values #
sensitive = true is only one layer of protection, and the weakest one. Stronger strategies start with how credentials are managed from the beginning.
# STRATEGY 1: Don't generate credentials in Terraform if you can avoid it
# Let the secrets manager manage and rotate credentials
# ANTI-PATTERN: Generate a password in Terraform
resource "random_password" "db" {
length = 16
special = true
}
resource "aws_db_instance" "main" {
password = random_password.db.result
# The password is stored in state — anyone with state access can read it
}
# CORRECT: Let RDS manage the password itself (AWS Secrets Manager rotation)
resource "aws_db_instance" "main" {
manage_master_user_password = true
# The password is managed by AWS Secrets Manager, not in Terraform state
}
# STRATEGY 2: Read secrets from the secrets manager, don't generate them
data "aws_secretsmanager_secret_version" "db" {
secret_id = "production/myapp/database"
}
locals {
db_creds = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}
resource "aws_db_instance" "main" {
username = local.db_creds.username
password = local.db_creds.password
# The secret isn't generated by Terraform, isn't "owned" by Terraform
# Terraform only reads and uses it
}
# STRATEGY 3: Output only what's needed, not entire credentials
# Instead of outputting the password directly, output a reference to the secrets manager
output "db_secret_arn" {
description = "ARN of the secret in Secrets Manager — use this to access credentials"
value = aws_secretsmanager_secret.db.arn
# No need for sensitive — an ARN isn't a credential, only a reference to one
}
# Applications/pipelines that need credentials will access Secrets Manager directly
# using this ARN, rather than reading them from a Terraform output
Auditing Sensitive Outputs in State #
Because state stores sensitive values in plaintext, it’s important to know where sensitive values are stored and to make sure state access is properly restricted.
# Check whether sensitive values are stored in state
terraform state pull | python3 -c "
import json, sys
state = json.load(sys.stdin)
for resource in state.get('resources', []):
for instance in resource.get('instances', []):
attrs = instance.get('attributes', {})
for key, val in attrs.items():
if any(keyword in key.lower() for keyword in ['password', 'secret', 'key', 'token']):
print(f\"{resource['type']}.{resource['name']}: {key} = ***\")
"
# Restrict state access with an IAM policy (S3 backend)
resource "aws_iam_policy" "terraform_state_read" {
name = "terraform-state-read"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:ListBucket"]
Resource = [
aws_s3_bucket.terraform_state.arn,
"${aws_s3_bucket.terraform_state.arn}/*"
]
# Restrict to principals that truly need state access
}
]
})
}
flowchart TD
A["🎯 Best Practices"] --> B["Mark sensitive outputs\nwith sensitive = true"]
A --> C["Don't commit the\nstate file to Git"]
A --> D["Encrypt state\nin the remote backend"]
A --> E["Restrict access\nto the state storage"]
A --> F["Use a secrets manager\nfor actual secrets"]
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#10b981,stroke:#059669,color:#fff
style C fill:#ef4444,stroke:#dc2626,color:#fff
style D fill:#f59e0b,stroke:#d97706,color:#fff
style E fill:#8b5cf6,stroke:#6d28d9,color:#fff
style F fill:#10b981,stroke:#059669,color:#fffAudit Trails for Sensitive Outputs #
Even when outputs are marked sensitive, access to them should still be audited for compliance.
# AWS CloudTrail can track who accesses the state
# containing sensitive values
# Check access to the S3 state bucket
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=ResourceName,AttributeValue=my-terraform-state \
--query 'Events[*].[EventTime,Username,EventName]' \
--output table
# Check access to the DynamoDB lock table
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=ResourceName,AttributeValue=terraform-state-lock \
--query 'Events[*].[EventTime,Username,EventName]' \
--output table
# Enable S3 access logging for the state bucket
resource "aws_s3_bucket_logging" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
target_bucket = aws_s3_bucket.audit_logs.id
target_prefix = "terraform-state-access/"
}
Sensitive Output Alternatives #
When sensitive outputs shouldn’t be stored in state at all, use an external secret manager.
# Instead of outputting the password from RDS:
# output "db_password" {
# value = aws_db_instance.main.password
# sensitive = true
# }
# Better: Generate the password with an external tool
resource "aws_secretsmanager_secret" "db_password" {
name = "${local.name_prefix}/db-password"
}
# The password is generated by Secrets Manager, NOT in state
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = jsonencode({
username = var.db_username
password = random_password.db.result
})
}
resource "random_password" "db" {
length = 32
special = true
}
# random_password.result IS still in state (because Terraform manages it)
# But in Secrets Manager, the password can be rotated without Terraform
flowchart LR
A["Terraform"] -->|"Generate"| B["random_password"]
B -->|"Store"| C["Secrets Manager"]
B -->|"Saved in state (sensitive)"| C
C -->|"App reads"| D["Application"]
style A fill:#e3f2fd,stroke:#1565c0
style C fill:#e8f5e9,stroke:#2e7d32
style D fill:#fff3e0,stroke:#e65100Sensitive Output Best Practices #
# BEST PRACTICE 1: Don't output passwords at all
# Store them directly in Secrets Manager
resource "aws_secretsmanager_secret" "db_creds" {
name = "${local.name_prefix}/database"
}
resource "aws_secretsmanager_secret_version" "db_creds" {
secret_id = aws_secretsmanager_secret.db_creds.id
secret_string = jsonencode({
host = aws_db_instance.main.address
port = aws_db_instance.main.port
username = var.db_username
password = random_password.db.result
})
}
# Output only the secret ARN (not its contents)
output "db_secret_arn" {
description = "ARN of the Secrets Manager entry containing DB credentials"
value = aws_secretsmanager_secret.db_creds.arn
}
# BEST PRACTICE 2: Use a secret reference in the application
# The app reads from Secrets Manager directly, not from Terraform outputs
aws secretsmanager get-secret-value --secret-id prod/database
Output Referencing #
# When an output is referenced from another stack:
# Make sure consumers know the output can change
# PRODUCER (networking stack):
output "subnet_ids" {
description = "List of subnet IDs - can change when networking is updated"
value = aws_subnet.private[*].id
}
# CONSUMER (compute stack):
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "tf-state"
key = "networking/terraform.tfstate"
region = "ap-southeast-1"
}
}
resource "aws_instance" "web" {
# Use a stable index
subnet_id = data.terraform_remote_state.networking.outputs.subnet_ids[0]
}
Summary #
sensitive = trueonly hides from the terminal — the value is still stored in plaintext in state and can still be read viaterraform output -raw.- State is the source of truth that needs securing — encryption at rest, restricted access via IAM, and S3 access audit logs matter more than the
sensitiveflag.- Avoid generating credentials in Terraform if possible — credentials generated by Terraform end up in state. Use managed rotation (like AWS Secrets Manager) when available.
- Read from a secrets manager, don’t store in state — let credentials be managed by a system designed for that purpose.
- Output references to the secrets manager (ARNs, IDs) are safer than outputting credentials directly — consumers fetch credentials themselves from the right source.
- Sensitivity “propagates” — values from sensitive variables or resources are automatically treated as sensitive by Terraform throughout their usage chain.