Learn OpenTofu - Secret Management & State Hardening
Episode 15 of 21

Learn OpenTofu - Secret Management & State Hardening

Secrets in IaC leak through three doors: plan output, plaintext state, and static credentials in code. This episode combines client-side state encryption with sensitive variables, then pulls dynamic secrets from Vault, AWS Secrets Manager, and GCP Secret Manager during apply.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In the previous episode 14 we reviewed managed platforms — Spacelift, env0, Scalr — and Digger. All those platforms run OpenTofu, but none of them can save you when secrets leak through things they treat as normal: state files, plan output, or sloppily written variables. Infrastructure security isn't determined by the platform, but by how secrets are stored, encrypted, and delivered to the engine.

In episode 15 we harden two layers at once. First, we combine client-side state encryption — first covered in episode 6 — with sensitive variables so secrets don't appear in plan or output. Second, we replace static credentials in code with dynamic secrets pulled directly during tofu apply from HashiCorp Vault, AWS Secrets Manager, and GCP Secret Manager.

Main Discussion

sensitive = true: Stopping Leaks in Plan and Output

Writing a password directly in variables.tf is a habit that must stop from day one. The right way: make that variable sensitive = true, so its value is never shown in tofu plan or tofu output, and never appears when rendered to screen:

variables.tf
variable "db_password" {
  type        = string
  sensitive   = true
  description = "Database password, supplied from a secret manager during apply"
}
 
output "db_connection_string" {
  value     = "${var.db_username}@${var.db_endpoint}"
  sensitive = true
}

Sensitive values make OpenTofu replace their contents with a red arrow in the plan output. Only one caveat to remember: sensitive = true only masks the display — the real value still rests intact in state.

Caution

Never put secret values in committed .tfvars files, because their trace lives forever in git history. In CI, supply sensitive variables via environment secrets or a .tfvars file generated by the pipeline and discarded after use.

Plaintext State Is a Silent Leak Door

That layer only controls display. The next layer is storage: state contains the real values of every resource, including passwords and keys. In episode 6 we already saw OpenTofu's solution — client-side state encryption, which encrypts state on the client before sending it to the backend, so even an S3 admin can't read its contents:

encryption.tf (recap from episode 6)
encryption {
  key_provider "aws_kms" "main" {
    kms_key_id = "arn:aws:kms:ap-southeast-1:123456789012:key/example-key-id"
  }
 
  method "aes_gcm" "main" {
    keys = key_provider.aws_kms.main
  }
 
  state {
    method = method.aes_gcm.main
  }
}

Combine both: sensitive = true keeps secrets from appearing in output and plan, while encryption keeps the same secrets from being readable in state. These are two sides of the same security door.

Dynamic Secrets: Vault at Apply Time

Static credentials inside .tf files — tokens or keys — are always a risk: copied to laptops, leaked to git, or lodging in CI caches. The solution is pulling secrets at apply time rather than storing them. The Vault provider reads secrets from HashiCorp Vault directly when OpenTofu evaluates the expression:

vault.tf
variable "vault_token" {
  type      = string
  sensitive = true
}
 
provider "vault" {
  address = var.vault_addr
  token   = var.vault_token
}
 
data "vault_generic_secret" "db" {
  path = "secret/data/db/prod"
}
 
resource "aws_db_instance" "postgres" {
  engine         = "postgres"
  username       = data.vault_generic_secret.db.data["username"]
  password       = data.vault_generic_secret.db.data["password"]
}

Notice the pattern: secrets are never stored as literals in code. OpenTofu reads the values from Vault during apply, and those values never appear in git history.

Native Cloud Alternatives: AWS and GCP Secret Manager

When the whole stack already runs on one cloud, using the cloud's built-in secret manager reduces the components you must manage. On AWS, the aws_secretsmanager_secret_version data source fetches the latest value of a secret, and the value can be a JSON blob you decode:

secretsmanager.tf
data "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/db-credentials"
}
 
resource "aws_db_instance" "postgres" {
  engine   = "postgres"
  username = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)["username"]
  password = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)["password"]
}

On GCP, google_secret_manager_secret_version fetches the active secret version and its value is available through the secret_data attribute:

secretmanager.tf
data "google_secret_manager_secret_version" "api_key" {
  secret = "api-key"
}
 
resource "google_cloudfunctions2_function" "api" {
  environment_variables = {
    API_KEY = data.google_secret_manager_secret_version.api_key.secret_data
  }
}
Secret sourceWhen it fitsOpenTofu data source
HashiCorp VaultMulti-cloud, active rotation, strict policyvault_generic_secret
AWS Secrets ManagerFull stack on AWSaws_secretsmanager_secret_version
GCP Secret ManagerFull stack on GCPgoogle_secret_manager_secret_version

Tip

Rotating secrets in a secret manager requires no code changes at all. When the value changes in Vault or AWS, just re-run tofu plan then tofu apply — the data source reads the latest version and dependent resources update automatically.

Safe Practices for Production

  • Always combine sensitive = true on variables and outputs; each has its own leak path.
  • Encrypt state on the client; never rely on the backend's default encryption alone.
  • Don't ship plan.tfplan files to public artifacts — a plan stores the same secret values as state.
  • In CI, authenticate to Vault with AppRole or OIDC, not personal tokens.
  • tofu console and tofu plan -no-color can display sensitive values — be careful when capturing logs.

Conclusion

In episode 15 we hardened secret handling:

  • Used sensitive = true on variables and outputs to stop leaks in plan and output.
  • Combined it with client-side state encryption so state is unreadable in the storage backend.
  • Pulled dynamic secrets from HashiCorp Vault via vault_generic_secret during apply.
  • Used aws_secretsmanager_secret_version and google_secret_manager_secret_version for cloud-native approaches.
  • Arranged safe practices: no secrets in git, no public plans, no personal tokens in CI.

Securing secrets is one thing; ensuring what may and may not be created in the cloud is another. In the next episode, episode 16, we'll build Policy as Code with Open Policy Agent: extracting the plan JSON with tofu show -json and writing Rego rules that reject security groups with port 22 open to the public or storage without encryption. See you there!

Learn OpenTofu - Secret Management & State Hardening | Learn OpenTofu