Secrets leak from Terraform in three places: code (a password in a .tf or .tfvars file), output (plan and CI logs) and state. Each tool below closes a different gap.
flowchart LR VS["Vault / Secrets Manager"] -->|"ephemeral resource"| EP["value used during the run only"] EP -->|"write-only argument"| RES["database password set"] RES --> ST["state: no password stored"] SV["sensitive variable"] --> LOG["plan output shows (sensitive value)"] %% step VS: The secret lives in a secret store, never in the repository. %% step EP: An ephemeral resource reads it for this run only; Terraform never writes it to plan or state. %% step RES: A write-only argument sends the value to the API without storing it. %% step ST: The state keeps only a version number, so reading the state reveals nothing. %% step SV: sensitive = true hides a value in CLI output and logs, but on its own it does not keep it out of state.
sensitive (hides output only)
variable "db_password" {
type = string
sensitive = true
}
output "db_url" {
value = "postgres://app:${var.db_password}@db:5432/shop"
sensitive = true # required: an output built from a sensitive value must be marked
}
Plan prints (sensitive value). The value is still in state in plain text, and terraform output -raw db_url still prints it.
ephemeral (Terraform 1.10+)
Ephemeral variables, outputs and resources exist only during a run and are never written to plan files or state.
ephemeral "random_password" "db" {
length = 24
}
variable "api_token" {
type = string
ephemeral = true # may only be used where ephemeral values are allowed
}
Ephemeral values can be used in provider configuration, in other ephemeral blocks, and in write-only arguments.
Write-only arguments (Terraform 1.11+)
resource "aws_db_instance" "shop" {
# ...
password_wo = ephemeral.random_password.db.result
password_wo_version = 1 # bump to rotate the password
}
The provider sends password_wo to the API but Terraform stores only password_wo_version. Look for _wo arguments in provider docs; not every resource has them yet.
Where secrets should come from
| Source | Ends up in state? |
|---|---|
Literal in .tf / committed .tfvars |
Yes, and in Git: never do this |
TF_VAR_db_password from the CI secret store |
Yes, if used in a normal argument |
data "aws_secretsmanager_secret_version" |
Yes: data source results are stored |
ephemeral "aws_secretsmanager_secret_version" + write-only argument |
No |
| Let the service generate and keep it (managed master password) | No |
Protect the state anyway
Even with all of this, older providers and many resources still store secrets. Encrypt the backend (S3 SSE-KMS), restrict who can read it, enable versioning and audit access, or use OpenTofu's client-side state encryption (topic 5.2).
Traps: nonsensitive() removes the protection and prints the value in logs; and an output that depends on a sensitive value fails with an error until you mark the output sensitive too.