Securing state, secrets & the supply chain

The IaC attack surface, closed.

Advanced14 min · lesson 15 of 15

Infrastructure code is an attack surface in its own right, and it concentrates risk in three places: the state file, the secrets that flow through Terraform, and the modules and providers you did not write. Securing Terraform means closing all three — because whoever controls your Terraform controls your infrastructure.

THE IaC ATTACK SURFACE
State
Encrypt at rest
remote backend + your own KMS key
Lock down who can read it
state read = credential read
Version it; never on laptop, Git, or CI logs
Secrets
Source at apply time from a manager
Vault, AWS Secrets Manager
Inject via TF_VAR_ env var in CI
Encrypt in-repo values with SOPS
Supply chain
Pin every module & provider to an exact version
Commit .terraform.lock.hcl
versions + checksums enforced
Review or vendor third-party modules
Whoever controls Terraform controls everything — defense in depth for the thing that builds everything else.

State is a secret — treat it like one

State stores resource attributes in plaintext, including database passwords, private keys, and generated credentials — so the state file is as sensitive as anything in it. Keep it in a remote backend that encrypts at rest, lock down who can read that bucket to a tight list (state read is credential read), enable versioning for recovery, and never write it to a laptop, a Git repo, or CI logs. A leaked state file is a leaked credential store.

backend.tf
terraform {
backend "s3" {
bucket = "acme-tf-state"
key = "prod/terraform.tfstate"
encrypt = true # SSE at rest
kms_key_id = "arn:aws:kms:...:key/..." # your own KMS key
dynamodb_table = "acme-tf-locks"
# bucket policy: only the tf-ci role and platform admins can read this prefix
}
}

Keep secrets out of code and state

Hard-coding a password in a .tf file commits it to Git forever; passing it through Terraform lands it in state anyway. The better pattern is to not put the secret in Terraform at all — read it at apply time from a secrets manager via a data source (Vault, AWS Secrets Manager), or inject it as a TF_VAR_ environment variable in CI from the secret store. For values that must live in a repo, encrypt them with SOPS so the committed file is ciphertext. Minimize what touches state, and encrypt what must.

main.tf
data "aws_secretsmanager_secret_version" "db" {
secret_id = "prod/db/password" # fetched at apply, from the manager
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db.secret_string
# still lands in state → encrypted backend + tight access are mandatory
}

The module & provider supply chain

Every external module and provider is code you run with full authority over your infrastructure — a malicious or compromised one could exfiltrate credentials or backdoor resources. Pin every module and provider to an exact version, commit the .terraform.lock.hcl so versions and checksums are enforced, prefer a private registry or vendored copies for critical modules, and review third-party modules before adopting them. This is the same supply-chain discipline as container images, applied to IaC.

Whoever controls Terraform controls everything
The Terraform pipeline, the state backend, and the provider credentials together form a tier-0 system — compromise any one and an attacker can reshape your entire estate. Close the loop: encrypted, access-controlled state; secrets sourced from a manager, never hard-coded; pinned and reviewed modules/providers with a committed lock file; and least-privilege, OIDC-based credentials in a protected pipeline. Defense in depth for the thing that builds everything else.