
How to Manage Secrets in Terraform
Secrets in Terraform come up in two distinct problems: how to supply sensitive values at plan/apply time without putting them in the codebase, and what to do about the state file that stores everything Terraform touched. Most teams solve the first problem but ignore the second until it causes a security incident.
This covers three approaches for supplying secrets, the non-negotiable state backend requirements, and when each method is the right fit.
Prerequisites
Before getting started, you should have:
- A working Terraform installation (0.14 or later, for the
sensitivevariable flag). - An AWS account with permissions to create KMS keys, Secrets Manager secrets, and RDS instances.
- The AWS CLI installed and configured.
- Basic familiarity with Terraform resources, variables, and data sources.
Goals
By the end of this post, you'll know how to:
- Use environment variables to keep secrets out of your codebase.
- Encrypt secret files with AWS KMS and SOPS for safe version control.
- Fetch secrets at runtime from AWS Secrets Manager.
- Choose the right approach based on your team size and security requirements.
Why Plain-Text Secrets in Terraform Are a Problem
Plain-text secrets in Terraform code do not just mean they are readable in the repository. Every CI/CD system that clones the repo stores a local copy. Every developer who has ever pulled the branch has it in their git history. Log aggregators that capture plan output may store the value. The blast radius of a single secret committed in plain text is much wider than it appears.
Always store secrets in encrypted form, regardless of whether the repository is private.
The Terraform State Problem
Regardless of the method you use to supply secrets to Terraform, there's one unavoidable concern: the Terraform state file.
Every time you run terraform apply, Terraform writes a terraform.tfstate file containing all the parameters it used, including any secrets, in plain text. This problem has existed for years, and there's no clean upstream fix yet.
There is no upstream fix for this. The practical mitigations are:
- Store Terraform state in an encrypted backend. Don't rely on a local
terraform.tfstatefile. Use backends like S3, Google Cloud Storage, or Azure Blob Storage, which support encryption in transit (TLS) and at rest (AES-256). - Strictly control access to your backend. Since state files can contain secrets, treat them like secrets themselves. If you use S3, configure an IAM policy that limits access to a small number of trusted developers, especially for production buckets.
Here's an example of an S3 backend configuration with encryption enabled:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "eu-west-1"
encrypt = true
kms_key_id = "arn:aws:kms:eu-west-1:123456789012:key/abcd-1234-efgh-5678"
}
}Note: Even with an encrypted backend, anyone with read access to the state can see secret values. Lock down your backend IAM policies as tightly as possible.
With those foundations in place, let's look at the methods for getting secrets into Terraform securely.
Method 1: Environment Variables
Terraform natively supports reading environment variables prefixed with TF_VAR_. This keeps plain-text secrets out of your code entirely.
How it works
First, define your sensitive variables in Terraform. Since Terraform 0.14, you can mark variables as sensitive, which prevents their values from appearing in plan or apply output:
variables.tf
variable "username" {
type = string
sensitive = true
}
variable "password" {
type = string
sensitive = true
}Then reference them in your resource:
main.tf
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
username = var.username
password = var.password
skip_final_snapshot = true
}Finally, set the values via environment variables before running Terraform:
export TF_VAR_username="root"
export TF_VAR_password="devops123"
terraform plan
terraform applyTip: On Linux and Mac, prefixing a command with a space prevents it from being saved in your Bash history. You can enable this behaviour by setting
HISTCONTROL=ignorespacein your shell profile.
Managing the secrets themselves
This method pushes the question of where to store secrets outside of Terraform. Common solutions include:
- 1Password or LastPass, SaaS password managers with CLI tools that can inject secrets into your shell environment.
- pass, an open-source CLI tool that stores each secret as a PGP-encrypted file, which can safely be committed to version control.
Pros and cons
Advantages:
- Keeps plain text out of code and version control.
- Easy to get started with.
- Integrates with most existing secrets management solutions.
- Test-friendly: you can set environment variables to mock values.
Drawbacks:
- Not everything is defined in the Terraform code itself, making it harder to understand and maintain.
- Everyone using the code must know to set these variables manually or run a wrapper script.
- No security guarantees enforced by Terraform. Someone could still manage secrets insecurely.
Method 2: Encrypted Files with AWS KMS and SOPS
Rather than relying on environment variables, you can encrypt your secrets, store the ciphertext in a file, and safely commit that file to version control. This way, secrets are versioned alongside your infrastructure code.
The key management question
Encrypting data requires an encryption key, and that key is itself a secret. The standard answer is to store the key in a managed key service provided by your cloud provider:
- AWS KMS (Key Management Service)
- GCP KMS (Cloud Key Management)
- Azure Key Vault
These services handle key rotation, access control, and audit logging for you.
Using AWS KMS directly
Start by creating a KMS key in the AWS console. Choose a symmetric key with encrypt/decrypt permissions. Then use the AWS CLI to encrypt a credentials file:
db-creds.yaml (the plain-text source, never committed to Git)
username: admin
password: s3cureP@ssw0rdEncrypt it:
aws kms encrypt \
--key-id <your-key-id> \
--plaintext fileb://db-creds.yaml \
--output text \
--query CiphertextBlob | base64 --decode > db-creds.yaml.encryptedThe resulting db-creds.yaml.encrypted file is safe to commit, even to a public repository. Only users with access to the KMS key can decrypt it.
To use it in Terraform, reference the aws_kms_secrets data source:
main.tf
data "aws_kms_secrets" "db_creds" {
secret {
name = "db"
payload = file("${path.module}/db-creds.yaml.encrypted")
}
}
locals {
db_creds = yamldecode(data.aws_kms_secrets.db_creds.plaintext["db"])
}
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
username = local.db_creds.username
password = local.db_creds.password
skip_final_snapshot = true
}Simplifying with SOPS
Manually running aws kms encrypt and aws kms decrypt for every edit is tedious and error-prone. SOPS (Secrets OPerationS) is an open-source tool that removes that friction. It automatically decrypts a file when you open it in your editor and re-encrypts it when you save.
Install SOPS and configure it to use your KMS key:
.sops.yaml
creation_rules:
- kms: "arn:aws:kms:eu-west-1:123456789012:key/abcd-1234-efgh-5678"Now you can edit encrypted files as if they were plain text:
sops db-creds.yamlSOPS will decrypt the file, open it in your $EDITOR, and re-encrypt it when you save and close.
If you use Terragrunt (a Terraform wrapper), it has native SOPS support built in:
terragrunt.hcl
locals {
db_creds = yamldecode(sops_decrypt_file("db-creds.yaml"))
}
inputs = {
username = local.db_creds.username
password = local.db_creds.password
}Pros and cons
Advantages:
- Plain-text secrets stay out of code and version control.
- Secrets are versioned, packaged, and tested alongside your code.
- Works with AWS KMS, GCP KMS, Azure Key Vault, and PGP.
- Everything is defined in code, no extra manual steps once SOPS is set up.
Drawbacks:
- Encrypting data requires extra tooling (SOPS) or verbose CLI commands.
- Learning curve to use these tools correctly.
- Rotating or revoking secrets is difficult. If the encryption key is ever compromised, an attacker could decrypt all historical secrets from your Git history.
- Minimal audit trail for who accessed secrets and when.
- Managed key services cost money (each AWS KMS key costs ~$1/month, plus per-request charges).
Method 3: Cloud Secret Stores (AWS Secrets Manager)
The most robust approach is to store secrets in a dedicated secret store, a database purpose-built for sensitive data with tight access controls, encryption, automatic rotation, and audit logging.
Setting it up
In the AWS console, create a new secret in AWS Secrets Manager:
- Choose the "Other type of secret" option.
- Store your credentials as a JSON key-value pair.
- Give it a descriptive name like
prod/db-creds.
You can also create it via the CLI:
aws secretsmanager create-secret \
--name "prod/db-creds" \
--secret-string '{"username":"admin","password":"s3cureP@ssw0rd"}'Using it in Terraform
main.tf
data "aws_secretsmanager_secret_version" "db_creds" {
secret_id = "prod/db-creds"
}
locals {
db_creds = jsondecode(data.aws_secretsmanager_secret_version.db_creds.secret_string)
}
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
username = local.db_creds.username
password = local.db_creds.password
skip_final_snapshot = true
}When you run terraform apply, Terraform fetches the secret at runtime and uses it in the resource configuration. The secret itself never touches your codebase or version control.
Why this is the strongest option
AWS Secrets Manager gives you several things the other methods don't:
- Automatic rotation. You can configure Secrets Manager to rotate credentials on a schedule using a Lambda function, with zero downtime.
- Fine-grained access control. IAM policies let you restrict which users, roles, and services can read each secret.
- Full audit trail. Every access is logged in AWS CloudTrail, so you always know who read or modified a secret and when.
- Cross-account sharing. You can grant access to secrets from other AWS accounts using resource-based policies.
Note: AWS Secrets Manager charges $0.40 per secret per month, plus $0.05 per 10,000 API calls. For most teams, this is a small price for the security and convenience it provides.
Conclusion
Here's a quick comparison of the three approaches:
| Method | Secrets in VCS? | Audit Trail | Rotation | Complexity |
|---|---|---|---|---|
| Environment Variables | No | None | Manual | Low |
| Encrypted Files (KMS/SOPS) | Yes (encrypted) | Minimal | Difficult | Medium |
| Secret Store (Secrets Manager) | No | Full | Easy | Medium |
The right choice depends on the context. Environment variables work for small teams and local development where the operational overhead of a full secret store is not justified. SOPS with KMS is useful when you need secrets versioned and auditable alongside your infrastructure code. AWS Secrets Manager is the right answer for production workloads where rotation, access scoping per IAM identity, and CloudTrail audit logs are requirements rather than nice-to-haves.
Regardless of which method you choose, always remember the two fundamentals:
- Never store secrets in plain text, in code, in commits, or on disk.
- Always use an encrypted, access-controlled backend for your Terraform state.
Comments