DEV Community

Mukami
Mukami

Posted on

How to Handle Sensitive Data Securely in Terraform

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"  # ❌
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Now the password never touches your code.


Leak Path 2: Variable Defaults

The mistake:

variable "db_password" {
  default = "super-secret-password"  # ❌ Still in code
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

What this gives you:

  • ✅ Secrets never appear in your .tf files
  • ✅ 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
}
Enter fullscreen mode Exit fullscreen mode

Plan output:

db_password = <sensitive>
Enter fullscreen mode Exit fullscreen mode

⚠️ 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What the Plan Looked Like

When I ran terraform plan, the output showed:

username = (sensitive value)
password = (sensitive value)
db_password = (sensitive value)
Enter fullscreen mode Exit fullscreen mode

The actual secrets never appeared in my terminal. But when I checked the state file:

$ cat terraform.tfstate | grep password
"password": "MySecurePassword123!"
Enter fullscreen mode Exit fullscreen mode

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:

  1. Using a secrets manager — AWS Secrets Manager, HashiCorp Vault
  2. Never hardcoding — no passwords in .tf files, no defaults
  3. Marking sensitive outputssensitive = true hides from terminal
  4. Securing the state file — encrypted S3 backend, restricted access
  5. 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
Enter fullscreen mode Exit fullscreen mode

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)