The Three Ways Secrets Leak (And How to Stop Every One)
Day 13 of the 30-Day Terraform Challenge — and today I learned that even when you think your secrets are safe, they're probably not.
Secrets leak in Terraform in three predictable ways. I found all three. I fixed all three. And I documented what I learned so you don't have to make the same mistakes.
The Three Leak Paths
Leak Path 1: Hardcoded in .tf Files
The mistake:
resource "aws_db_instance" "example" {
username = "admin"
password = "super-secret-password" # ❌
}
This password is now in your Git history. Forever. Even if you delete it, it's still in the commit history. Anyone with access to your repo can see it.
The fix:
variable "db_password" {
type = string
sensitive = true
# No default — Terraform will prompt
}
Now the password never touches your code.
Leak Path 2: Variable Defaults
The mistake:
variable "db_password" {
default = "super-secret-password" # ❌ Still in code
}
Default values are stored in your .tf files. Same problem as hardcoding. The secret is right there in version control.
The fix: No defaults for secrets. Ever.
Leak Path 3: The State File
The reality: Even if you fix the first two, Terraform stores every resource attribute in terraform.tfstate in plaintext.
$ cat terraform.tfstate | grep password
"password": "MySecurePassword123!" # ❌ Right there!
Anyone with read access to the state file can see all your secrets.
The fix:
- Use remote state with encryption (S3 +
encrypt = true) - Restrict access with IAM policies
- Never commit state to Git
- Enable versioning to recover from mistakes
AWS Secrets Manager: The Right Way
Instead of hardcoding, store secrets in AWS Secrets Manager:
data "aws_secretsmanager_secret" "db_credentials" {
name = "prod/db/credentials"
}
data "aws_secretsmanager_secret_version" "db_credentials" {
secret_id = data.aws_secretsmanager_secret.db_credentials.id
}
locals {
db_credentials = jsondecode(
data.aws_secretsmanager_secret_version.db_credentials.secret_string
)
}
resource "aws_db_instance" "example" {
username = local.db_credentials["username"]
password = local.db_credentials["password"]
}
What this gives you:
- ✅ Secrets never appear in your
.tffiles - ✅ Fetched at runtime
- ✅ Centralized secret management
- ✅ Rotation capabilities
Marking Secrets as Sensitive
Terraform's sensitive = true prevents secrets from appearing in terminal output:
output "db_password" {
value = local.db_credentials["password"]
sensitive = true
}
Plan output:
db_password = <sensitive>
⚠️ Important: This does NOT prevent secrets from being stored in state. It only hides them from the terminal.
State File Security Checklist
After deploying, I verified all of these:
✅ S3 backend with encryption:
terraform {
backend "s3" {
bucket = "my-terraform-state"
encrypt = true # AES-256 encryption
}
}
✅ Block public access on S3 bucket
✅ Enable versioning to recover from mistakes
✅ Restrict IAM policy to only Terraform runners
✅ .gitignore to prevent accidental commits:
.terraform/
*.tfstate
*.tfstate.backup
*.tfvars
What the Plan Looked Like
When I ran terraform plan, the output showed:
username = (sensitive value)
password = (sensitive value)
db_password = (sensitive value)
The actual secrets never appeared in my terminal. But when I checked the state file:
$ cat terraform.tfstate | grep password
"password": "MySecurePassword123!"
The password was right there in plaintext. This was the "aha!" moment — state encryption isn't optional. It's mandatory.
Chapter 6 Learnings
Does sensitive = true prevent secrets from being stored in state?
No. It only hides them from terminal output. Secrets are still in state. You must secure the state file.
Vault vs Secrets Manager:
- AWS Secrets Manager: AWS-native, simpler, good for AWS-only environments, integrates with RDS
- HashiCorp Vault: Multi-cloud, dynamic secrets, fine-grained policies, more complex to set up
Why secrets appear in state: Terraform stores all resource attributes to track infrastructure. The only solution is to secure the state file itself with encryption and access controls.
What I Learned
Secrets management isn't just about not hardcoding passwords. It's about:
- Using a secrets manager — AWS Secrets Manager, HashiCorp Vault
-
Never hardcoding — no passwords in
.tffiles, no defaults -
Marking sensitive outputs —
sensitive = truehides from terminal - Securing the state file — encrypted S3 backend, restricted access
- Proper .gitignore — never commit state or tfvars
The state file is the last line of defence. Secure it like you would a password manager.
My .gitignore
# Terraform
.terraform/
.terraform.lock.hcl
*.tfstate
*.tfstate.backup
*.tfvars
*.tfvars.json
# Local secrets
*.secret
secrets.tfvars
# Environment
.env
.env.local
The Bottom Line
Three leak paths. Three fixes:
| Leak Path | Fix |
|---|---|
| Hardcoded in .tf | Use variables with sensitive = true
|
| Variable defaults | Remove defaults, use Secrets Manager |
| State file | Encrypted remote backend, restricted access |
Security isn't optional in production infrastructure.
Resources:
P.S. The moment I saw my password in the state file was humbling. No matter how careful you are with your code, if you don't secure your state, you're leaving secrets in plain sight.
Top comments (0)