CoursesInfrastructure as Code & automationAdvanced · Testing & secrets

Secrets in IaC: SOPS & Vault

Keep credentials out of state and code.

Advanced12 min · lesson 22 of 23

IaC has a specific, dangerous secrets problem: the code and the state both routinely end up holding credentials in plaintext. A database password in a .tf variable, a provider API key hardcoded, or — the sneaky one — a secret that Terraform writes into its state file as a resource attribute. The rule from every other course applies with force here: secrets must never be committed to the repo or left readable in state. Solving it means both keeping secrets out of the code and protecting the state that may capture them anyway.

Where IaC leaks secrets, and the fix
the leaks
hardcoded in .tf
committed to Git
in a committed .tfvars
same problem
captured in state
plaintext in tfstate
the fixes
fetch at runtime
Vault provider / cloud secrets
encrypt files
SOPS for values files
protect state
encrypted remote backend, tight IAM
Two fronts: keep secrets OUT of code (fetch them), and protect the STATE that may capture them regardless.

Fetch secrets at runtime, do not store them

The strongest approach is to never put the secret in the code at all — fetch it at apply time from a secrets manager. Terraform’s Vault provider (and the cloud secrets-manager data sources) pull a credential from Vault or AWS/GCP Secrets Manager during the run, so the code references "the database password in Vault" rather than the value. Even better, authenticate the pipeline to the cloud via OIDC short-lived credentials (the CI lesson’s pattern) so there is no long-lived cloud key stored anywhere either. The secret lives in one managed place; the code just points at it.

terminal / main.tf
# reference a secret from Vault instead of hardcoding it:
data "vault_kv_secret_v2" "db" { mount = "secret", name = "prod/db" }
resource "aws_db_instance" "main" {
password = data.vault_kv_secret_v2.db.data["password"] # fetched at apply, not in code
}
# and encrypt any secrets that must live in a file, with SOPS:
$ sops --encrypt --age $AGE_KEY secrets.dec.yaml > secrets.enc.yaml # commit the ENCRYPTED file

SOPS and protecting state

When a secret genuinely must live in a file you commit (some config values), SOPS (Secrets OPerationS) encrypts just the values — you commit an encrypted file, and it is decrypted only at use with a key from KMS/age/Vault, so the repo never holds plaintext. And because Terraform may still capture some secrets in state regardless of how carefully you fetch them, the second front is non-negotiable: a remote backend that encrypts state at rest, with tight IAM so only the pipeline and the right people can read it. You minimize what reaches state, and you protect the state that does.

Terraform state can hold secrets even when your code does not
The trap that catches careful teams: even if you fetch a secret from Vault at runtime, Terraform often writes that value into the state file as a resource attribute (a database password, a generated key) — so the plaintext ends up in state anyway. This is why "keep secrets out of code" is only half the answer; you MUST also protect state as sensitive: encrypted remote backend, restricted IAM, no committing tfstate, and awareness that anyone who can read state can read those secrets. Assume state contains secrets and guard it accordingly, and it ties straight into the dedicated Secrets management course.