Environment & Secret #
Terraform often needs to know about sensitive things: database passwords, API keys for third-party providers, private certificates. The problem is that Terraform stores everything it manages in the state file — including sensitive values. And the state file can be read by anyone with access to the backend. Managing secrets in Terraform securely means understanding what goes into state, what doesn’t, and how to design the configuration so secrets don’t spread further than they should.
flowchart LR
A["Password\nAPI Key\nCertificate"] --> B["Terraform"]
B --> C["State File\n(plaintext!)"]
B --> D["Cloud Resources\n(encrypted)"]
style A fill:#f59e0b,stroke:#d97706,color:#fff
style B fill:#8b5cf6,stroke:#6d28d9,color:#fff
style C fill:#ef4444,stroke:#dc2626,color:#fff
style D fill:#10b981,stroke:#059669,color:#fffWhat Goes into State #
One common misunderstanding is thinking that sensitive = true on a variable or output hides the value from state. This is not true.
WHAT GOES INTO THE TERRAFORM STATE:
resource "aws_db_instance" "main" {
identifier = "production-db"
password = var.db_password ← THIS PASSWORD GOES INTO STATE
}
After terraform apply:
terraform.tfstate contains:
{
"resources": [{
"type": "aws_db_instance",
"instances": [{
"attributes": {
"password": "MyS3cr3tP4ssword" ← PLAINTEXT in state
}
}]
}]
}
sensitive = true ONLY hides from:
- Terminal output (terraform output, terraform plan)
- The terraform plan display
sensitive = true does NOT hide from:
- The state file (still plaintext)
- terraform output -raw <output_name>
- Anyone with access to the state backend
Don’t Generate Secrets in Terraform If You Can Avoid It #
The safest way to manage secrets is to not create secrets in Terraform at all. Let the secret be created and rotated outside Terraform, and Terraform only reads its reference.
# ANTI-PATTERN: Generating a password in Terraform
resource "random_password" "db" {
length = 32
special = true
}
resource "aws_db_instance" "main" {
password = random_password.db.result # The password enters state
}
# CORRECT: The password is created outside Terraform (manually or via a secrets manager)
# Terraform only reads the ARN, not the value
# Create the secret in AWS Secrets Manager (once, outside Terraform)
# aws secretsmanager create-secret --name prod/db/password --secret-string "..."
# Terraform only references the secret ARN, not its value
data "aws_secretsmanager_secret" "db_password" {
name = "prod/db/password"
}
resource "aws_db_instance" "main" {
# Use the ARN reference to Secrets Manager, not the password value
manage_master_user_password = true
# Or: use the AWS-managed master_user_secret
}
Reading Secrets from AWS Secrets Manager #
When Terraform truly needs to use a secret value (for example, to configure a resource that needs a password), read it from Secrets Manager as a data source.
# Read the secret from Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/master-password"
}
# Use the secret value
resource "aws_db_instance" "main" {
identifier = "production-db"
engine = "postgres"
instance_class = "db.t3.medium"
# This value enters state — a trade-off to be aware of
# Make sure state is encrypted and state access is very restricted
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# Better: use AWS-managed manage_master_user_password
# so the password never appears in state at all
}
# The best way for RDS: let AWS manage the password
resource "aws_db_instance" "main_v2" {
identifier = "production-db"
engine = "postgres"
instance_class = "db.t3.medium"
username = "admin"
manage_master_user_password = true
# AWS creates the password and stores it in Secrets Manager
# The password does NOT enter Terraform state
# Rotation can be configured via master_user_secret_rotation
}
AWS SSM Parameter Store for Configuration #
SSM Parameter Store is suitable for configuration that isn’t secret but needs to be accessed by many services — endpoint URLs, feature flags, environment configuration.
# Store configuration in SSM Parameter Store
resource "aws_ssm_parameter" "db_endpoint" {
name = "/prod/database/endpoint"
type = "String" # Not a secret — String is enough
value = aws_db_instance.main.endpoint
}
resource "aws_ssm_parameter" "api_key" {
name = "/prod/external/api-key"
type = "SecureString" # Secret — encrypted with KMS
value = var.external_api_key
# The value still enters state — but it's encrypted in SSM
}
# Read the parameter from SSM (in another configuration or in an application)
data "aws_ssm_parameter" "db_endpoint" {
name = "/prod/database/endpoint"
}
output "db_endpoint" {
value = data.aws_ssm_parameter.db_endpoint.value
}
Passing Secrets via Environment Variables #
Secrets needed at runtime (not at provisioning time) should be passed via environment variables, not configured in Terraform.
# ANTI-PATTERN: Hardcoding a secret in a resource
resource "aws_ecs_task_definition" "app" {
container_definitions = jsonencode([{
name = "app"
image = "myapp:latest"
environment = [
{
name = "DB_PASSWORD"
value = "MyP4ssword" # ✗ Enters state as plaintext
}
]
}])
}
# CORRECT: Reference the secret from Secrets Manager, not the value
resource "aws_ecs_task_definition" "app" {
container_definitions = jsonencode([{
name = "app"
image = "myapp:latest"
# Don't use 'environment' for secrets
# Use 'secrets' which reads from Secrets Manager at runtime
secrets = [
{
name = "DB_PASSWORD"
valueFrom = "arn:aws:secretsmanager:ap-southeast-1:123456789:secret:prod/db/password"
# The ECS agent reads this value when the container starts
# The value never enters Terraform state
}
]
}])
}
Securing State That Contains Secrets #
If state already contains secrets (a common situation for many existing configurations), make sure state access is very restricted.
# S3 backend with encryption and strict access
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "ap-southeast-1"
# Encryption required
encrypt = true
kms_key_id = "arn:aws:kms:ap-southeast-1:123456789:key/abc-123"
# Access via DynamoDB lock
dynamodb_table = "terraform-state-lock"
}
}
# IAM Policy for the S3 state bucket:
# - Only TerraformApplyRole can read/write state
# - Engineering teams can only list the bucket, not read state
# - Audit all access via CloudTrail
flowchart TD
A["Secret in config"] --> B{"Method?"}
B -->|"In state"| C["Backend encryption"]
B -->|"Reference only"| D["Secrets Manager"]
D --> E["data source read"]
E --> F["Not in state\n✅"]
style A fill:#f59e0b,stroke:#d97706,color:#fff
style B fill:#f97316,stroke:#ea580c,color:#fff
style C fill:#ef4444,stroke:#dc2626,color:#fff
style D fill:#3b82f6,stroke:#1e40af,color:#fff
style E fill:#8b5cf6,stroke:#6d28d9,color:#fff
style F fill:#10b981,stroke:#059669,color:#fffEncrypt-at-Rest for Secrets in State #
Secrets in the state file must be encrypted at rest. Make sure the backend configuration enables encryption.
# S3 backend with KMS encryption
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
kms_key_id = aws_kms_key.terraform_state.arn
dynamodb_table = "terraform-state-lock"
}
}
resource "aws_kms_key" "terraform_state" {
description = "KMS key for Terraform state"
deletion_window_in_days = 30
enable_key_rotation = true
}
# Verify encryption is active
aws s3api get-bucket-encryption --bucket my-terraform-state
# Must return SSEAlgorithm: aws:kms
# Verify KMS key rotation is active
aws kms get-key-rotation-status --key-id $KEY_ID
# Must return true
Secret Rotation Strategy #
Secrets should be rotated periodically to minimize the risk if a secret leaks.
# Rotation strategies:
# 1. Automatic rotation using AWS Secrets Manager
resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db.id
rotation_lambda_arn = aws_lambda_function.rotate.arn
rotation_rules {
automatically_after_days = 30
}
}
# 2. Manual rotation (for secrets that can't auto-rotate)
# a. Generate a new secret
# b. Update it in the secret manager
# c. Deploy the application with the new secret
# d. Verify the application works
# e. Disable the old secret
# f. Delete the old secret after a grace period
flowchart TD
A["Secret Manager\n(rotation schedule)"] --> B["Trigger rotation\n(every 30 days)"]
B --> C["Lambda: generate\na new secret"]
C --> D["Update the secret\nin Secrets Manager"]
D --> E["App: read the\nnew secret"]
E --> F["Verify: the app\nworks"]
F --> G["Disable the\nold secret"]
style A fill:#e3f2fd,stroke:#1565c0
style G fill:#ffebee,stroke:#c62828Secret Manager Integration #
# AWS Secrets Manager with Terraform
resource "aws_secretsmanager_secret" "app" {
name = "${local.name_prefix}/app-config"
description = "Application configuration secrets"
# Enable rotation
rotation_lambda_arn = aws_lambda_function.rotate.arn
rotation_rules {
automatically_after_days = 30
}
}
resource "aws_secretsmanager_secret_version" "app" {
secret_id = aws_secretsmanager_secret.app.id
secret_string = jsonencode({
database_url = "postgres://${var.db_user}:***@${aws_db_instance.main.address}:5432/app"
redis_url = "redis://${aws_elasticache_cluster.main.cache_nodes[0].address}:6379"
jwt_secret = random_password.jwt.result
})
}
Summary #
sensitive = trueonly hides from terminal output, not from the state file — sensitive values remain stored in plaintext in state.- Avoid generating secrets in Terraform if possible — let AWS Secrets Manager or dedicated tools manage the secret lifecycle.
- Use
manage_master_user_passwordon RDS — AWS manages the password and stores it in Secrets Manager without the value entering Terraform state.- SSM Parameter Store for non-secret configuration needing access by many services; Secrets Manager for credentials and secrets needing encryption and rotation.
- ECS secrets vs environment — use
secrets(Secrets Manager references) notenvironmentfor sensitive values in task definitions; the ECS agent reads the value at runtime, it doesn’t enter state.- Secure state backend access with KMS encryption and strict IAM policies — this is the last line of defense because state can contain plaintext secrets.
← Previous: Provider Authentication Next: Credential Rotation Strategy →