Managing Secrets
Handle sensitive data safely in Terraform configurations
Managing Secrets
In the previous tutorial, we learned about resource dependencies and lifecycle rules. Now let's talk about the thing that ends up on the news when you get it wrong.
Passwords, API keys, tokens β your infrastructure needs them, but they absolutely should NOT live in your Terraform files or state. Let's look at the right ways to handle sensitive data so you don't become the subject of a "company leaks credentials on GitHub" headline.
The Problem
"What's the big deal? I'll just put the password in the config file."
Please don't.
# DON'T DO THIS
resource "aws_db_instance" "main" {
identifier = "production"
username = "admin"
password = "SuperSecret123!" # Now it's in Git forever
}
Secrets in code end up in:
- Version control history (where they live forever)
- Terraform state files
- CI/CD logs
- Team member machines
Once a secret hits Git, consider it compromised. Even if you delete the commit, it's still in the history. Rotate it immediately.
Sensitive Variables
"Can I at least hide passwords from the terminal output?"
Yes! Mark variables as sensitive:
variable "db_password" {
type = string
description = "Database password"
sensitive = true
}
resource "aws_db_instance" "main" {
identifier = "production"
username = "admin"
password = var.db_password
}
Output:
# terraform plan
+ resource "aws_db_instance" "main" {
+ password = (sensitive value)
}
Sensitive Outputs
output "db_password" {
value = aws_db_instance.main.password
sensitive = true
}
Without sensitive = true, Terraform refuses to output it.
Sensitive Locals
locals {
connection_string = "postgresql://${var.db_user}:${var.db_password}@${aws_db_instance.main.endpoint}/mydb"
}
output "connection_string" {
value = local.connection_string
sensitive = true
}
Environment Variables
"So if I can't put secrets in files, how do I pass them in?"
Environment variables. Classic and effective:
# Set the variable
export TF_VAR_db_password="SuperSecret123!"
# Run Terraform
terraform apply
Terraform automatically picks up TF_VAR_* environment variables. No files, no commits, no problems.
variable "db_password" {
type = string
sensitive = true
# No default β must be provided. Good. Passwords shouldn't have defaults.
}
In CI/CD
# GitHub Actions
jobs:
deploy:
steps:
- name: Terraform Apply
env:
TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
run: terraform apply -auto-approve
AWS Secrets Manager
"Is there a proper place to store secrets in the cloud?"
Absolutely. AWS Secrets Manager is built for exactly this.
Creating Secrets
resource "aws_secretsmanager_secret" "db_password" {
name = "production/db/password"
description = "Production database password"
tags = {
Environment = "production"
}
}
resource "random_password" "db" {
length = 32
special = true
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = random_password.db.result
}
Reading Secrets
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/db/password"
}
resource "aws_db_instance" "main" {
identifier = "production"
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
JSON Secrets
# Store multiple values
resource "aws_secretsmanager_secret_version" "db_creds" {
secret_id = aws_secretsmanager_secret.db_creds.id
secret_string = jsonencode({
username = "admin"
password = random_password.db.result
host = aws_db_instance.main.endpoint
})
}
# Read and decode
data "aws_secretsmanager_secret_version" "db_creds" {
secret_id = "production/db/credentials"
}
locals {
db_creds = jsondecode(data.aws_secretsmanager_secret_version.db_creds.secret_string)
}
# Use
resource "aws_db_instance" "main" {
username = local.db_creds.username
password = local.db_creds.password
}
AWS SSM Parameter Store
"Secrets Manager costs money. Any free alternatives?"
SSM Parameter Store! Simpler and cheaper (free for standard parameters):
Store Parameters
resource "aws_ssm_parameter" "db_password" {
name = "/production/db/password"
description = "Production database password"
type = "SecureString"
value = random_password.db.result
tags = {
Environment = "production"
}
}
Read Parameters
data "aws_ssm_parameter" "db_password" {
name = "/production/db/password"
}
resource "aws_db_instance" "main" {
password = data.aws_ssm_parameter.db_password.value
}
Parameter Hierarchy
# Organized by path
aws_ssm_parameter.this["/myapp/production/db/host"]
aws_ssm_parameter.this["/myapp/production/db/user"]
aws_ssm_parameter.this["/myapp/production/db/password"]
aws_ssm_parameter.this["/myapp/production/api/key"]
# Read all parameters under a path
data "aws_ssm_parameters_by_path" "config" {
path = "/myapp/production"
recursive = true
with_decryption = true
}
Secrets Manager vs SSM Parameter Store
| Feature | Secrets Manager | Parameter Store |
|---|---|---|
| Cost | $0.40/secret/month | Free (standard) |
| Rotation | Built-in | Manual |
| Cross-account | Easy | Possible |
| Size limit | 64KB | 8KB (advanced) |
| Versioning | Automatic | Automatic |
Use Secrets Manager for: Database credentials, API keys that need automatic rotation Use Parameter Store for: Configuration values, simple secrets, saving money
Random Password Generation
"Can Terraform generate passwords for me?"
Don't invent passwords β let the robot do it:
resource "random_password" "db" {
length = 32
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
resource "aws_db_instance" "main" {
password = random_password.db.result
}
# Store it somewhere retrievable
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = random_password.db.result
}
Password Won't Change on Apply
Random provider resources only change when their inputs change:
resource "random_password" "db" {
length = 32
special = true
# Force rotation
keepers = {
rotation_time = timestamp() # Changes every apply!
}
}
Better approach β rotate on schedule:
resource "random_password" "db" {
length = 32
special = true
keepers = {
# Rotate monthly
rotation_month = formatdate("YYYY-MM", timestamp())
}
}
The State File Problem
"Wait⦠even with sensitive variables, the password ends up in state?!"
Yep. This is the dirty secret about Terraform secrets. Even with sensitive = true, secrets end up in state:
{
"resources": [
{
"type": "aws_db_instance",
"values": {
"password": "SuperSecret123!"
}
}
]
}
Solutions
- Encrypt state at rest (S3 backend with encryption)
- Restrict state access (IAM policies)
- Use managed secrets (reference ARN, not value)
Reference ARN Instead
# Instead of putting password in Terraform state...
resource "aws_rds_cluster" "main" {
master_password = aws_secretsmanager_secret_version.db.secret_string # In state!
}
# Better: Let RDS read from Secrets Manager
resource "aws_rds_cluster" "main" {
manage_master_user_password = true
master_user_secret_kms_key_id = aws_kms_key.db.key_id
}
RDS 2023+ can manage its own secrets in Secrets Manager. How cool is that? The password never even touches Terraform state.
Vault Integration
"We're an enterprise. We need something more serious."
HashiCorp Vault for enterprise secret management:
provider "vault" {
address = "https://vault.example.com"
}
data "vault_generic_secret" "db" {
path = "secret/data/production/database"
}
resource "aws_db_instance" "main" {
username = data.vault_generic_secret.db.data["username"]
password = data.vault_generic_secret.db.data["password"]
}
Dynamic Secrets
# Vault generates temporary AWS credentials
data "vault_aws_access_credentials" "creds" {
backend = "aws"
role = "deploy"
}
provider "aws" {
access_key = data.vault_aws_access_credentials.creds.access_key
secret_key = data.vault_aws_access_credentials.creds.secret_key
}
SOPS for Encrypted Files
Encrypt secrets files with SOPS:
# Encrypt with AWS KMS
sops --encrypt --kms arn:aws:kms:us-west-2:123456789:key/abc secrets.yaml > secrets.enc.yaml
# Read encrypted file
data "sops_file" "secrets" {
source_file = "secrets.enc.yaml"
}
resource "aws_db_instance" "main" {
password = data.sops_file.secrets.data["db_password"]
}
Commit secrets.enc.yaml to Git safely. The encrypted version is useless without the KMS key.
Best Practices
The "please don't get your company on the news" section.
1. Never Commit Secrets
# .gitignore
*.tfvars
!*.example.tfvars
secrets/
.env
2. Use Different Secrets Per Environment
data "aws_secretsmanager_secret_version" "db" {
secret_id = "${var.environment}/db/password"
}
3. Rotate Regularly
resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db.id
rotation_lambda_arn = aws_lambda_function.rotate_secret.arn
rotation_rules {
automatically_after_days = 30
}
}
4. Audit Access
resource "aws_secretsmanager_secret" "db" {
name = "production/db/password"
# CloudTrail logs all access
}
# Alert on secret access
resource "aws_cloudwatch_metric_alarm" "secret_access" {
alarm_name = "secret-access-alarm"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "CallCount"
namespace = "AWS/SecretsManager"
period = 300
statistic = "Sum"
threshold = 10
dimensions = {
SecretId = aws_secretsmanager_secret.db.id
}
}
5. Least Privilege
# App can only read specific secrets
data "aws_iam_policy_document" "app_secrets" {
statement {
actions = ["secretsmanager:GetSecretValue"]
resources = [
"arn:aws:secretsmanager:*:*:secret:production/app/*"
]
}
}
Example: Complete Setup
# Generate password
resource "random_password" "db" {
length = 32
special = true
}
# Store in Secrets Manager
resource "aws_secretsmanager_secret" "db" {
name = "${var.environment}/database/master"
}
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = jsonencode({
username = "dbadmin"
password = random_password.db.result
})
}
# Use in RDS
resource "aws_db_instance" "main" {
identifier = "${var.environment}-db"
username = jsondecode(aws_secretsmanager_secret_version.db.secret_string)["username"]
password = jsondecode(aws_secretsmanager_secret_version.db.secret_string)["password"]
# ...
}
# Grant app access
resource "aws_iam_role_policy" "app_secrets" {
name = "secrets-access"
role = aws_iam_role.app.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [aws_secretsmanager_secret.db.arn]
}]
})
}
What's Next?
Secrets are no joke β and you now know how to handle them like a professional. You learned:
- Sensitive variables and outputs
- Environment variables for CI/CD
- AWS Secrets Manager and SSM Parameter Store
- Random password generation
- The state file problem and how to mitigate it
- Vault and SOPS for enterprise setups
Now let's tackle environment management β workspaces and environments. Let's go!