DEV Community

Cover image for How to Handle Sensitive Data Securely in Terraform
Victor Robin
Victor Robin

Posted on

How to Handle Sensitive Data Securely in Terraform

Every real-world infrastructure deployment involves secrets—database passwords, API keys, and TLS certificates. The number one security mistake engineers make is letting those secrets leak into their codebase or terminal outputs.

For todays Terraform Challenge, I built an impenetrable wall around my infrastructure's sensitive data using AWS Secrets Manager.

Here is the definitive guide to the three ways secrets leak in Terraform, and exactly how to close every single path.


Leak Path 1: Hardcoded in .tf Files

The Mistake: Writing a secret directly into a resource argument. The moment you run git add, that password is permanently stored in your version control history for anyone in your organization (or the public) to see.

Vulnerable Pattern:


resource "aws_db_instance" "app_database" {
  username = "admin"
  password = "SuperSecretPassword123!" # Never do this!
}

Enter fullscreen mode Exit fullscreen mode

Secure Alternative (AWS Secrets Manager):

Instead of typing the password, create it manually in AWS Secrets Manager. Then, use Terraform data blocks to fetch it dynamically at runtime.

# 1. Fetch the secret from AWS

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
}

# 2. Decode the JSON string
locals {
  db_creds = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)
}

# 3. Inject it at runtime
resource "aws_db_instance" "app_database" {
  username = local.db_creds["username"]
  password = local.db_creds["password"]
}

Enter fullscreen mode Exit fullscreen mode

Result: The secret never exists in your configuration files.


Leak Path 2: Passed as a Variable with a Default

The Mistake: Moving the secret to variables.tf, but giving it a default value. Default values are still committed to source control, meaning the leak still happens.

Vulnerable Pattern:


variable "db_password" {
  type    = string
  default = "SuperSecretPassword123!" # Still committed to Git!
}

Enter fullscreen mode Exit fullscreen mode

Secure Alternative (sensitive = true):

Remove the default value completely. Furthermore, add the sensitive = true flag. This prevents Terraform from accidentally printing the secret to the terminal screen when you run terraform plan or terraform apply.


variable "db_password" {
  description = "Database administrator password"
  type        = string
  sensitive   = true 
  # Terraform will now require this to be passed securely via TF_VAR_ environment variables or a secure CI/CD pipeline.
}

output "db_connection_string" {
  value     = "mysql://${local.db_creds["username"]}:${local.db_creds["password"]}@${aws_db_instance.app_database.endpoint}"
  sensitive = true # Outputs <sensitive> instead of the raw string in the CLI
}

Enter fullscreen mode Exit fullscreen mode

Leak Path 3: Stored in Plaintext in the State File

The Mistake: Even if you use AWS Secrets Manager and the sensitive = true flag perfectly, Terraform still stores the final evaluated values of your resources in terraform.tfstate in plaintext. If an attacker gains access to your state file, they have the keys to your kingdom. sensitive = true only hides it from the terminal; it does not encrypt the state file.

Secure Alternative (The State File Security Audit):
Because secrets inevitably end up in state, the state file itself must be locked down using a remote backend (like AWS S3) with strict security boundaries.

My Production S3 Backend Security Checklist:

  1. Server-Side Encryption: Enabled (encrypt = true and AES256 default encryption rules on the bucket).

  2. Versioning: Enabled (to recover from accidental state corruption).

  3. Public Access Blocked: block_public_acls and block_public_policy strictly enforced.

  4. State Locking: DynamoDB table attached to prevent concurrent write corruption, or use S3 nataive lockfile settings use_lockfile = true

  5. Least Privilege IAM: Only specific CI/CD roles are allowed s3:GetObject on the state bucket.

Finally, to ensure no local state files or variable files are ever accidentally committed to GitHub, your .gitignore must always include:

# Terraform Security Gitignore
.terraform/
*.tfstate
*.tfstate.backup
*.tfvars
override.tf
Enter fullscreen mode Exit fullscreen mode

Security is not optional in production infrastructure. Master your secrets management before you deploy, not after a breach.


Top comments (0)