Handling Secrets and Sensitive Data in Terraform

Managing infrastructure as code (IaC) requires access to highly sensitive information. Databases need passwords, cloud providers require API keys, and virtual machines need SSH private keys. Hardcoding these credentials directly into your configuration files is one of the most dangerous security mistakes you can make.

In this guide, you will learn why secrets management in Terraform is unique, how Terraform handles sensitive data, and the best practices for keeping your infrastructure secure from development to production.

The Core Problem: Terraform State and Plaintext Secrets

Before diving into solutions, it is critical to understand how Terraform processes data. When you run Terraform, it tracks the state of your managed infrastructure in a file called terraform.tfstate.

Crucial Security Warning: Terraform state files store all resource attributes in plaintext. This includes any passwords, private keys, or tokens generated or used during the deployment. Even if you mark a variable as sensitive, it will still be written in plaintext inside the state file. Therefore, securing the state file is your absolute first line of defense.

+-------------------------------------------------------------+
|                     Terraform Workflow                      |
+-------------------------------------------------------------+
|                                                             |
|  [Your TF Code] ---> [Terraform Engine] ---> [Cloud Provider]|
|       |                     |                               |
|       v                     v                               |
|  (Masks CLI outputs)   (Writes EVERYTHING in plaintext)     |
|                             |                               |
|                             v                               |
|                     [terraform.tfstate]                     |
|                      *Plaintext Secrets*                    |
|                                                             |
+-------------------------------------------------------------+

Method 1: Using the Sensitive Variable Attribute

Terraform provides a built-in way to prevent sensitive values from being printed to the console during terraform plan or terraform apply. By setting the sensitive argument to true on an input variable, you instruct Terraform to mask this value in the CLI output.

Example: Defining a Sensitive Input Variable


variable "db_password" {
  type        = string
  description = "The password for the database administrator."
  sensitive   = true
}

resource "aws_db_instance" "database" {
  allocated_storage   = 20
  engine              = "mysql"
  instance_class      = "db.t3.micro"
  username            = "admin"
  password            = var.db_password
  skip_final_snapshot = true
}

When you run a plan with this configuration, Terraform will display (sensitive value) instead of exposing the actual password in your terminal. However, remember that the password is still fully visible in the terraform.tfstate file generated after application.

Method 2: Injecting Secrets via Environment Variables

To avoid committing secrets to your version control system (like Git), you should never hardcode default values for sensitive variables in your .tf files. Instead, you can inject them at runtime using environment variables.

Terraform automatically reads environment variables that start with the prefix TF_VAR_. This is a highly recommended approach for local development and CI/CD pipelines.

Step-by-Step Implementation

First, declare the variable in your Terraform code without a default value:


variable "api_token" {
  type      = string
  sensitive = true
}

Next, export the variable in your terminal before running Terraform commands:


export TF_VAR_api_token="prod-xyz-987654321-token"
terraform apply

This keeps the secret out of your codebase entirely, reducing the risk of accidental Git commits containing credentials.

Method 3: Integrating with External Secret Managers

For production environments, relying on environment variables can become hard to manage and audit. The industry standard is to fetch secrets dynamically at runtime from dedicated secret management tools like HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, or Google Cloud Secret Manager.

In this workflow, Terraform acts as a client that reads the secret during the plan/apply phase, uses it to configure resources, and never stores the secret in the version control repository.

Example: Reading a Secret from AWS Secrets Manager


# 1. Retrieve the metadata of the secret
data "aws_secretsmanager_secret" "db_secret_metadata" {
  name = "production/database/password"
}

# 2. Retrieve the actual payload (version) of the secret
data "aws_secretsmanager_secret_version" "db_secret_payload" {
  secret_id = data.aws_secretsmanager_secret.db_secret_metadata.id
}

# 3. Use the secret value in a resource
resource "aws_db_instance" "prod_db" {
  allocated_storage = 50
  engine            = "postgres"
  instance_class    = "db.r6g.large"
  username          = "db_admin"
  
  # The secret payload is often stored as a JSON string
  password          = jsondecode(data.aws_secretsmanager_secret_version.db_secret_payload.secret_string)["password"]
}

By using data sources, you ensure that the secret is fetched fresh during every Terraform run. If the security team rotates the password in AWS Secrets Manager, Terraform will automatically detect the change on the next run.

Common Mistakes and How to Avoid Them

  • Mistake 1: Committing .tfvars files containing secrets. Developers often use terraform.tfvars files to test locally. If you put secrets here and commit them to Git, your credentials are compromised. Solution: Always add *.tfvars and *.tfvars.json to your global .gitignore file.
  • Mistake 2: Assuming "sensitive = true" encrypts the state file. As emphasized, this attribute only masks terminal output. Solution: Use secure remote backends like AWS S3 with KMS encryption, Azure Blob Storage, or HashiCorp Cloud Platform (HCP) Terraform which encrypt state files at rest.
  • Mistake 3: Storing local state files on shared drives. Leaving terraform.tfstate on your local machine or an unencrypted shared network folder exposes plain text secrets to anyone with access. Solution: Enforce remote state locking and encryption for all team projects.

Real-World Use Cases

Use Case 1: Automated CI/CD Pipelines

In modern DevOps pipelines (such as GitHub Actions, GitLab CI, or Jenkins), hardcoded cloud credentials are a major security vulnerability. By combining Terraform with OpenID Connect (OIDC), pipelines can assume temporary IAM roles dynamically. Any additional database or API credentials needed by Terraform are injected securely via masked pipeline environment variables or pulled directly from Vault using short-lived tokens.

Use Case 2: Multi-Tenant Infrastructure Isolation

When deploying identical infrastructure stacks for different clients, you must ensure that client-specific keys and passwords remain isolated. Using Terraform workspaces combined with dynamic secret lookups based on the workspace name (e.g., fetching secrets/${terraform.workspace}/db_pass) ensures that no client credentials are mixed or exposed across environments.

Interview Notes for DevOps Engineers

  • Question: Does marking a variable as sensitive = true protect it inside the state file?
  • Answer: No. It only prevents the value from being printed to the stdout/CLI console during plan and apply. The value remains in plaintext inside the terraform.tfstate file. To secure the state file, you must use a secure remote backend that supports encryption-at-rest and strict IAM access controls.
  • Question: How do you handle secrets rotation in Terraform?
  • Answer: By fetching secrets dynamically using data sources (like aws_secretsmanager_secret_version or HashiCorp Vault provider). When a secret is rotated in the vault, the next terraform apply will detect the change, update the resource with the new value, and update the state file accordingly.
  • Question: What is the best practice for local development secrets?
  • Answer: Use environment variables prefixed with TF_VAR_, or use a local system key-manager integration. Never write secrets in local .tfvars files unless those files are explicitly ignored in .gitignore.

Summary

Securing secrets in Terraform requires a multi-layered approach. While the sensitive = true attribute is excellent for keeping logs and terminal screens clean, it does not secure your backend storage. True security is achieved by combining environment variables, dynamic secret managers, strict .gitignore rules, and highly secure, encrypted remote state backends.