Skip to content

Security Concepts


Terraform can create networks, databases, IAM policies, compute resources, and other infrastructure. That means Terraform is not merely a configuration file sitting beside your infrastructure. It is part of the security boundary.

A production Terraform design therefore has to protect not only the infrastructure it creates, but also the configuration, state, credentials, dependencies, execution environment, and automation pipeline used to create it.

Treat Terraform As Part Of The Security Boundary

A useful way to think about Terraform security is:

Configuration
     β”‚
     β”œβ”€β”€ credentials
     β”œβ”€β”€ secrets
     β”œβ”€β”€ provider configuration
     β”œβ”€β”€ modules/providers
     β”‚
     β–Ό
Terraform Execution
     β”‚
     β”œβ”€β”€ state
     β”œβ”€β”€ logs
     └── CI/CD
     β”‚
     β–Ό
Infrastructure

Every stage can expose sensitive information or grant powerful permissions.

A security failure therefore does not have to be an insecure cloud resource. It can be:

  • a cloud credential committed to Git
  • a remote state bucket readable by the wrong team
  • a secret printed in CI logs
  • an untrusted provider version
  • excessive permissions granted to a CI identity
  • a sensitive value copied into a plan artifact

Security needs to be considered across the whole workflow.

Protect Terraform State

Terraform state can contain considerably more information than the values you explicitly output.

A state file may contain:

  • resource identifiers
  • network information
  • configuration details
  • database settings
  • generated values
  • connection information
  • secret values returned by providers

Marking an output as sensitive does not remove its value from state.

For example:

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

output "database_password" {
  value     = var.database_password
  sensitive = true
}

The CLI will avoid displaying the value normally, but Terraform still needs the value to manage the configuration. It can therefore remain present in state.

This leads to an important distinction:

MechanismWhat it does
sensitive = trueReduces accidental display in Terraform output
EncryptionProtects data from unauthorized reading while stored or transmitted
Secret-management systemProvides a dedicated system for storing and controlling secrets
Short-lived credentialLimits how long stolen credentials remain useful

These mechanisms solve different problems. None should be treated as a replacement for the others.

Secure Remote State

Production state should normally be stored remotely using a backend with appropriate:

  • encryption at rest
  • encrypted transport
  • authentication
  • authorization
  • access logging
  • backup/versioning controls
  • restricted access

A state backend should be treated as a sensitive infrastructure system, not as an ordinary application data bucket.

State access should follow least privilege. Someone who only needs to work on development infrastructure should not automatically receive unrestricted production state access.

Warning

A backup copy of state is also sensitive. Encrypting the primary state location does not protect an unencrypted copy sitting in a developer laptop, CI artifact store, backup system, or temporary directory.

Protect Credentials

Avoid embedding long-lived credentials directly in Terraform files.

For example, do not build a configuration around:

provider "aws" {
  access_key = "long-lived-secret"
  secret_key = "another-secret"
}

Prefer the provider’s supported credential chain and environment or identity mechanisms.

For CI/CD, a stronger model is often short-lived identity:

CI job
  β”‚
  β”œβ”€β”€ proves its identity
  β”‚
  β–Ό
Cloud identity service
  β”‚
  └── issues short-lived credentials
          β”‚
          β–Ό
      Terraform

OIDC-style authentication is useful because the CI system can exchange a trusted workload identity for temporary cloud credentials instead of storing a permanent cloud secret in the repository.

The exact setup depends on the cloud provider and CI platform, but the security principle is portable:

Prefer short-lived credentials tied to an authenticated workload over permanent credentials stored in CI secrets.

Separate Environments

Development and production should not normally share one unrestricted cloud identity.

A safer arrangement is:

Development Terraform
        β”‚
        └── development permissions

Production Terraform
        β”‚
        └── production permissions

A compromised development pipeline should not automatically become a production administration path.

Production approval boundaries can add another layer:

Pull Request
     ↓
Plan
     ↓
Review
     ↓
Approval
     ↓
Apply with production identity

This creates separation between writing a change and authorizing its execution.

Sensitive Values Can Leak In Unexpected Places

Consider:

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

resource "terraform_data" "example" {
  input = var.database_password
}

output "database_password" {
  value     = var.database_password
  sensitive = true
}

Terraform can hide the output value from normal CLI display, but the value can still exist in state.

It can also leak through surrounding tooling.

For example, this is dangerous:

terraform apply -var="database_password=super-secret-password"

The command itself can end up in shell history, process inspection, CI logs, or other execution metadata depending on the environment.

A safer workflow is to provide sensitive values through an appropriate secret mechanism supported by your execution environment rather than putting them directly into command arguments.

The same caution applies to debugging:

TF_LOG=TRACE terraform plan

Detailed logs can contain information that should not be exposed publicly.

Warning

Never assume that sensitive = true makes a secret safe everywhere. It mainly changes how Terraform presents the value. You still need to secure state, credentials, logs, CI systems, and source control.

Protect Providers And Modules

Terraform dependencies are part of the software supply chain.

Your configuration may depend on:

  • providers
  • registry modules
  • Git-based modules
  • other external sources

Treat those dependencies as code you are trusting to execute with significant infrastructure permissions.

Pin provider and module versions deliberately.

For providers, Terraform records selected versions and checksums in .terraform.lock.hcl. Commit that file to Git for reproducible provider installation.

A typical provider constraint might be:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

The exact version range should reflect your upgrade policy rather than blindly tracking every release.

Module versioning should receive similar attention. A production root module should not silently change behavior because an upstream dependency changed unexpectedly.

Supply-chain security is partly about knowing exactly what code and provider versions your Terraform execution trusts.

Protect Git History

Deleting a secret from the current working tree does not necessarily remove it from Git history.

A common accident looks like:

commit 1: secret added
commit 2: secret removed

The secret may still exist in commit 1.

If a real credential is committed, treat it as compromised and rotate or revoke it. Removing the file from the latest commit is not sufficient.

Also review:

  • .gitignore
  • generated Terraform files
  • local variable files
  • plan artifacts
  • state backups
  • CI artifacts

A typical Terraform .gitignore contains entries such as:

.terraform/
*.tfstate
*.tfstate.*
*.tfvars
*.tfvars.json
crash.log
crash.*.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json

Be careful with *.tfvars: some teams intentionally commit non-secret variable files. The important rule is to prevent actual secrets and sensitive state from entering source control.

A Practical Secret-Exposure Example

Imagine a developer creates:

variable "api_token" {
  type      = string
}

output "api_token" {
  value = var.api_token
}

They then run:

terraform apply -var="api_token=secret-value"

There are several exposure points:

Command line
    β”‚
    β”œβ”€β”€ shell history
    β”œβ”€β”€ process metadata
    └── CI command logs

Terraform output
    β”‚
    └── visible token

Terraform state
    β”‚
    └── token recorded for management

A safer design starts by making the output sensitive:

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

output "api_token" {
  value     = var.api_token
  sensitive = true
}

Then provide the value through the execution environment’s secret mechanism rather than a visible command argument.

Finally, restrict state access and avoid exposing state or plan artifacts to people or systems that do not need them.

This does not make the secret magically disappear from Terraform state. It reduces unnecessary exposure at the other layers.

A Production Security Checklist

Before treating a Terraform project as production-ready, ask:

  • Is state stored remotely and protected?
  • Is state encrypted?
  • Who can read production state?
  • Who can modify production state?
  • Are provider credentials short-lived where practical?
  • Does CI use workload identity rather than permanent cloud credentials?
  • Are production permissions separated from development permissions?
  • Are provider versions constrained?
  • Is .terraform.lock.hcl committed?
  • Are module sources and versions trusted and controlled?
  • Can secrets enter Git history?
  • Can secrets appear in CI logs?
  • Can plan artifacts expose sensitive values?
  • Are backups and copies of state protected?
  • Is there an appropriate production approval boundary?

Terraform security is therefore not one setting. It is a chain of controls protecting the entire infrastructure-as-code workflow.

Last updated on