What Terraform, OpenTofu, and Packer promise about secrets, and where each promise stops
The Ansible Vault entry on this site was framed around a boundary Ansible states outright: encryption there "ONLY protects data at rest." Terraform's equivalent — the sensitive argument — makes no such promise, and it's worth being precise about which of the three tools can keep a value off disk at all. Terraform and OpenTofu can, with features other than sensitive. Packer can't, because it has no state file to keep a value out of.
The state file is where Terraform records what it created, attribute by attribute, so the next plan can compare it with the configuration. The provider is the plugin that talks to the actual API (AWS, Proxmox and so on). Both are introduced in the Terraform/OpenTofu fundamentals entry.
sensitive keeps nothing out of state, and Terraform's own docs say so
sensitive = true on a variable looks like the HCL cousin of Ansible's !vault tag, which marks one encrypted value in a YAML file — mark the value, and Terraform hides it:
variable "db_password" {
type = string
sensitive = true
}
Plan output shows (sensitive value) instead of the string, and any expression that depends on a sensitive value inherits the same masking. That's the entire feature. Per Terraform's own docs on the subject, stated as plainly as Ansible's own vault warning:
Terraform will still record sensitive values in the state, and so anyone who can access the state data will have access to the sensitive values in cleartext.
sensitive is a display filter over the CLI, nothing more — the same category of protection as Podman's file secrets driver being "read-protected" rather than encrypted, already covered on this site. The docs go further, naming a second gap in the same section: "A sensitive variable is a configuration-centered concept, and values are sent to providers without any obfuscation. A provider error could disclose a value if that value is included in the error message." A masked plan diff says nothing about what a provider's own stack trace might print.
Ephemeral values: the thing that actually keeps a value out of state
An ephemeral value exists only for the duration of one plan or apply. Terraform holds it in memory, passes it where it's needed, and never writes it to the state file or to a saved plan. When the run ends, the value is gone. It can come from three places:
- ephemeral input variables, set by whoever runs the command;
- ephemeral resources, which fetch or create something only for the run, such as reading a secret from Vault or generating a password;
- ephemeral outputs, which pass such a value out of a module.
The two earlier Terraform entries on this site name the feature as a coming topic; this is where it's covered.
Terraform 1.10 added this property, which does what sensitive never did: it keeps the value out of the artifact entirely.
variable "session_token" {
type = string
ephemeral = true
}
Per Terraform's variables docs: "Terraform omits ephemeral values from state and plan files... unlike sensitive inputs, Terraform ensures ephemeral values are not available beyond the lifetime of the current Terraform run." That's a real, structural difference from sensitive, not a stronger flavor of the same thing.
The tradeoff is that an ephemeral value is contagious and narrowly usable. Referencing one anywhere makes the referencing expression ephemeral too — "local.database_password is implicitly ephemeral because it depends on var.password" — and an ephemeral value can only be used in specific contexts: write-only arguments, other ephemeral variables and outputs, locals, ephemeral resources, provider configuration blocks, and provisioner and connection blocks. Anywhere else, Terraform errors rather than silently keeping the value around.
Write-only arguments, and the workaround their statelessness requires
Ephemeral variables need somewhere to actually land on a resource, and that's what write-only arguments (1.11+) are for — a resource argument whose value Terraform hands to the provider and then discards, never writing it to state or plan:
resource "aws_db_instance" "example" {
# ...
password_wo = ephemeral.random_password.db_password.result
password_wo_version = 1
}
The mechanism this creates is worth tracing rather than assuming: without a stored value, Terraform has nothing to diff a write-only argument against on the next run. Per Terraform's own write-only argument docs: "Terraform does not store write-only arguments in state files, so Terraform has no way of knowing if a write-only argument value has changed. Because Terraform cannot track write-only argument values, it sends write-only arguments to the provider during every operation." Every plan, every apply, whether or not the password actually changed — statelessness here isn't free, it's a standing cost paid on every run. The paired _version argument (password_wo_version) is how a provider recovers change detection despite that. Terraform does store the version number, and per the same page, "To trigger an update of a write-only argument, increment the version argument's value in your configuration" — the provider then uses the new value to update the resource. The write-only argument itself carries no signal at all.
flowchart TD
RUN["Every plan or apply"] --> CHECK{"password_wo_version<br/>changed since last state?"}
CHECK -->|No| NOOP["password_wo still sent to the provider,<br/>but no update is planned"]
CHECK -->|Yes| UPDATE["Plan shows an update;<br/>provider applies the new password_wo"]
NOOP --> STATE["Terraform discards password_wo;<br/>only password_wo_version persists to state"]
UPDATE --> STATE
Local state was always plain-text JSON, and remote state is a maybe
None of the above changes a fact that predates ephemeral values entirely. Per Terraform's own docs on sensitive data in state: "When using local state, state is stored in plain-text JSON files." Remote state fares better only conditionally — "It may be encrypted at rest, but this depends on the specific remote state backend." On the S3 backend, the encrypt option asks for server-side encryption explicitly — S3 has encrypted new objects with SSE-S3 (server-side encryption with keys that S3 manages) by default since January 2023, but a key you control in AWS KMS, Amazon's key-management service, still has to be configured. HCP Terraform, HashiCorp's hosted Terraform service, "always encrypts state at rest," per the same page — but a backend choice is not a language feature, and nothing enforces it. Ephemeral values and write-only arguments exist specifically because this baseline was never going to change: the only way to keep a value out of a plain-text state file is to never let Terraform write it there in the first place.
OpenTofu's actual answer, and what it still doesn't cover
This is the fork divergence the site's Terraform/OpenTofu fundamentals entry already flagged as a headline feature without detailing it — and it's the one item from that queue worth pulling forward here, since it's squarely a secrets feature rather than a full OpenTofu-only survey (that comparison is its own future entry). Per OpenTofu 1.7.0's own CHANGELOG, verbatim: "We're introducing optional end-to-end encryption for state files." The encryption method is AES-GCM, a standard authenticated cipher: a tampered file fails to decrypt instead of decrypting to garbage. A key provider is where the encryption key comes from. The original ones are a passphrase (stretched into a key with PBKDF2), the AWS and GCP key-management services (KMS), and OpenBao, the open-source fork of HashiCorp Vault. Azure Key Vault has since joined the list. Terraform has no equivalent; this is the actual, load-bearing "handful of things OpenTofu genuinely has" fact, not a footnote. OpenTofu doesn't trade away the rest to get it, either: per its 1.11.0 CHANGELOG, "Ephemeral values, ephemeral resources, and write-only attributes are now supported", so everything in the two sections above applies to tofu as well.
Even here, OpenTofu's own docs are careful about exactly what the feature buys, in a caveat that lands in the same place as everything above: "OpenTofu does not and cannot protect the sensitive values in the state file from the person running the tofu command." State encryption closes the stolen-file threat — an attacker who gets the state file off disk or out of a bucket gets ciphertext. It does nothing about the operator who is supposed to be running tofu plan in the first place, which is exactly the boundary Ansible's own vault guide draws around "data at rest" too: a promise about a file sitting still, not about anyone legitimately allowed to touch it.
Packer has no state file, and its sensitive flag says so by what it doesn't claim
Packer's own sensitive argument reads almost identically to Terraform's:
variable "admin_password" {
sensitive = true
default = "SECR3TP4SSW0RD"
}
Per Packer's own docs, what it actually does is narrower even than Terraform's version: "all string-values from that variable will be obfuscated from Packer's output" — demonstrated with exactly one example, packer inspect. Nothing in the same page addresses the console output of provisioners, the scripts or Ansible runs that configure the machine during a build (the Packer fundamentals entry covers them). Nor does it say anything about the artifact packer build produces.
That gap is structural, not an oversight. Terraform and OpenTofu both have a state file to protect and a well-defined "run" boundary to keep a value inside. Packer has neither, by design: its one artifact is the image itself, and once a provisioner writes a value into that image's filesystem, into a baked-in config file, or into its own shell history inside the build, there's no equivalent of "data at rest" left to protect it. The closest thing to a real secrets story Packer has is its vault() HCL function, which reaches out to an actual running HashiCorp Vault server at build time — vault("secret/data/hello", "foo") — paired with a local block's own sensitive = true to keep the fetched value out of console output. It's a real integration, but it answers "where does the secret come from," not "what happens to it once a provisioner has it." That second question is entirely on whatever script runs inside the build.
The shape underneath all three
Terraform's sensitive and Packer's sensitive share a naming trap that Ansible Vault doesn't: each looks like the strong guarantee, and each is actually the weak one — a display filter, not an encryption boundary. Ansible Vault really does encrypt data at rest; Terraform and Packer only reach that level with other features. The features that do the real work are named for what they specifically exclude — ephemeral, write-only, OpenTofu's encryption block — not for the general idea of secrecy. Reaching for the term that sounds most like "secret" is the reliable way to reach for the wrong tool in these HCL tools.
Created 2026-09-29T13:29:28+02:00 · Edit