Day 13 of my Terraform journey focused on one of the most important topics in real infrastructure work: secrets.
Every serious deployment eventually needs sensitive values:
- database passwords
- API keys
- tokens
- TLS material
- provider credentials
The challenge is not just using those secrets. The challenge is making sure they do not leak into places they should never be.
Terraform makes infrastructure easy to define, but if you are careless with secrets, they can leak through your code, your terminal output, your Git history, and even your state file.
This post is the guide I wish I had before learning this lesson.
Why Secrets Leak in Terraform
There are three major ways secrets leak in Terraform.
If you understand these clearly, you will avoid most beginner and intermediate Terraform security mistakes.
Leak Path 1: Hardcoded in .tf Files
This is the most obvious mistake.
Wrong
resource "aws_db_instance" "example" {
username = "admin"
password = "super-secret-password"
}
Why this is bad:
- the password is now in source control
- if you run
git add, the secret is part of Git history - even if you delete it later, the old commit still contains it
Once a secret is committed, you must treat it as compromised.
Better
Do not write the secret directly in Terraform code.
Instead, fetch it from a secret store such as 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" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
db_name = "appdb"
username = local.db_credentials["username"]
password = local.db_credentials["password"]
allocated_storage = 10
skip_final_snapshot = true
}
This keeps the secret out of your .tf files.
Leak Path 2: Secret Stored as a Variable Default
This one looks cleaner, but it is still wrong.
Wrong
variable "db_password" {
default = "super-secret-password"
}
Why this is bad:
- the secret is still in source control
- it still ends up in the file
- it is just hidden behind a variable name
A variable default is not a secure secret store.
Better
If a variable may carry a secret:
- do not give it a default
- mark it sensitive
variable "db_password" {
description = "Database administrator password"
type = string
sensitive = true
}
Then pass the value from:
- environment variables
- your CI/CD secret store
- a secure runtime mechanism
This helps reduce accidental exposure in code and CLI output.
Leak Path 3: Plaintext in Terraform State
This is the most important and most overlooked problem.
Even if you do everything else correctly:
- no hardcoded secrets
- no secret defaults
- values pulled from Secrets Manager
- outputs marked as sensitive
Terraform can still store secret values in terraform.tfstate.
That means:
- anyone with access to the state file may be able to read secrets
- protecting state is just as important as protecting code
This is what separates security-aware Terraform usage from surface-level secret handling.
AWS Secrets Manager Integration Pattern
For Day 13, I used AWS Secrets Manager as the secret source.
First, create the secret manually:
aws secretsmanager create-secret \
--name "prod/db/credentials" \
--secret-string '{"username":"dbadmin","password":"your-secure-password-here"}'
Then read it in Terraform:
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
)
}
Then use it inside your resource:
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 10
db_name = "appdb"
username = local.db_credentials["username"]
password = local.db_credentials["password"]
skip_final_snapshot = true
}
Why this is better:
- the secret is not hardcoded in Terraform
- the secret stays centralized in Secrets Manager
- secret updates can be managed outside the codebase
But remember:
- this still does not eliminate the state-file risk
HashiCorp Vault Integration Pattern
For teams already using Vault, the pattern is similar.
Instead of pulling from AWS Secrets Manager, Terraform can read from Vault.
Example pattern:
data "vault_generic_secret" "db_credentials" {
path = "secret/data/prod/db"
}
locals {
db_username = data.vault_generic_secret.db_credentials.data["username"]
db_password = data.vault_generic_secret.db_credentials.data["password"]
}
Then use those values in resources just like any other data source.
Why Vault is useful:
- strong secret lifecycle management
- centralized policy control
- dynamic secrets in more advanced setups
- useful when teams already standardize on Vault across environments
The same warning still applies:
- if Terraform consumes the value in a resource, the secret can still end up in state
Using sensitive = true
Terraform provides a sensitive = true flag for variables and outputs.
Example variable:
variable "db_password" {
description = "Database administrator password"
type = string
sensitive = true
}
Example output:
output "db_connection_string" {
value = "mysql://${aws_db_instance.example.username}@${aws_db_instance.example.endpoint}"
sensitive = true
}
What it does:
- prevents Terraform from printing the raw value in plan/apply output
- helps keep logs and terminal output cleaner
What it does not do:
- it does not encrypt the secret
- it does not remove it from state
- it does not make a bad secret-handling design safe
So sensitive = true is helpful, but it is not enough on its own.
Provider Credentials: Use Environment Variables
Never put provider credentials directly in Terraform code.
Wrong
provider "aws" {
region = "us-east-1"
access_key = "AKIA..."
secret_key = "..."
}
Better
Use environment variables:
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_DEFAULT_REGION="us-east-1"
This is much safer because:
- credentials stay out of
.tffiles - CI/CD systems can inject them securely
- they are easier to rotate than hardcoded values
In real environments, even better options are:
- IAM roles
- OIDC
- short-lived credentials
State File Security Checklist
Because secrets can land in state, protecting the state file is mandatory.
Use this checklist:
- store state remotely, not just on a laptop
- enable S3 encryption
- enable bucket versioning
- block all public access
- restrict bucket access with least-privilege IAM
- enable DynamoDB locking
- keep state files out of Git
- treat anyone with state access as having potential secret access
A good backend pattern looks like this:
terraform {
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "production/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
.gitignore Template for Terraform Projects
Every Terraform project should ignore these files:
# Terraform
.terraform/
.terraform.lock.hcl
*.tfstate
*.tfstate.backup
*.tfvars
override.tf
override.tf.json
This reduces the chance of accidentally committing:
- local cache files
- state files
- variable files with secrets
Least-Privilege IAM for the State Bucket
Your state bucket should not be wide open.
At minimum, access should be limited to:
- the IAM user/role that runs Terraform
- only the necessary S3 actions
- only the specific state bucket and key paths
A simple principle is:
- allow read/write to the Terraform state bucket
- allow lock-table access to the DynamoDB table
- deny everyone else
The exact policy will vary by environment, but the mindset is the important part:
- state is sensitive
- access to state should be tightly controlled
My Main Takeaway
Day 13 changed how I think about Terraform security.
The big lesson was not just:
βdonβt hardcode passwords.β
The real lesson was:
- secrets can leak in code
- secrets can leak through defaults
- secrets can still leak through state even when the code looks clean
That means secure Terraform work requires both:
- better secret input patterns
- better state protection
Using AWS Secrets Manager and sensitive = true is a strong start.
But the deeper lesson is this:
if you protect the code but ignore the state file, you have not really solved the problem.
Full Code
GitHub reference:
π Github Link
Follow My Journey
This is Day 13 of my 30-Day Terraform Challenge.
See you on Day 14 π
Top comments (0)