
Run terraform state pull | grep -i password against almost any real-world state file and you'll find one. Not because anyone did anything wrong, but because that's how Terraform's state model has worked since the beginning: every attribute of every resource gets written to state so Terraform can compute a diff on the next plan, and a database password is an attribute like any other. Marking it sensitive = true doesn't change that. It only tells the CLI not to print the value; the state file, and any plan file saved with -out, still get the plaintext.
This isn't a Terraform-specific problem. OpenTofu inherited the same state model, since it forked from Terraform 1.5.7. But the two projects have since built genuinely different fixes, on different timelines, and understanding both matters if you're running either engine in production.
sensitive = true doesn't do what people thinkvariable "db_password" {
type = string
sensitive = true
}
resource "aws_db_instance" "app" {
engine = "postgres"
username = "app"
password = var.db_password
}This looks safe. It isn't. terraform plan and terraform apply will print (sensitive value) instead of the password, but Terraform still needs the real value to configure the resource, and it still records that value as part of aws_db_instance.app's attributes in state, because a future plan needs to know the resource's full current attribute set to detect drift. Pull the state file and the password is right there in plain JSON.
The same applies to plan files. It's common in CI/CD to run terraform plan -out=tfplan, upload tfplan as a pipeline artifact, then run terraform apply tfplan in a later stage after a human approves it. That plan file is a complete snapshot of the operation Terraform is about to perform, secrets included. Treat it with the same access controls you'd put on the state file itself, not as a disposable build artifact.
Terraform 1.10 (November 2024) introduced ephemeral resource blocks. Terraform 1.11 (February 2025) followed with write-only arguments on managed resources. Together they let a value exist only for the duration of the current operation and never get written to state or plan files at all, which is a different strategy than hiding a value that's already there.
ephemeral "random_password" "app_db" {
length = 20
special = true
}
resource "aws_db_instance" "app" {
engine = "postgres"
username = "app"
skip_final_snapshot = true
password_wo = ephemeral.random_password.app_db.result
password_wo_version = 1
}password_wo is a write-only argument: the aws provider receives the generated password, uses it to create the database, and Terraform discards the value afterward without persisting it anywhere. Because Terraform never stores a write-only value, it also can't diff it on the next plan, so providers pair each write-only argument with an ordinary, state-tracked version counter (password_wo_version here). Bump the version and the provider knows to apply the new write-only value; leave it alone and Terraform leaves the resource as-is.
Two things to know before you reach for this:
_wo and _wo_version suffixes are the convention.ephemeral resource, or with an ephemeral read from a secrets manager, is the actual point.OpenTofu approached the same problem from the opposite direction. Instead of starting with per-field exclusion, it shipped whole-file encryption first.
State and plan encryption landed in OpenTofu 1.7, April 2024, more than six months before Terraform's ephemeral resources. It's a terraform block in your configuration, not a CLI flag, so it applies uniformly to everything a workspace writes:
terraform {
encryption {
key_provider "pbkdf2" "main" {
passphrase = var.state_encryption_passphrase
}
method "aes_gcm" "default" {
key_provider = key_provider.pbkdf2.main
}
state {
method = method.aes_gcm.default
}
plan {
method = method.aes_gcm.default
}
}
}Beyond the pbkdf2 passphrase provider shown here, OpenTofu also supports AWS KMS, GCP KMS, and OpenBao as key providers, so enterprises can tie state encryption to an existing key-management setup instead of a shared passphrase. Once enabled, OpenTofu refuses to read a state file that isn't encrypted with the configured method, which is a deliberate guardrail against a downgrade silently exposing plaintext state again.
Where OpenTofu was ahead of Terraform on whole-file encryption, it was behind on per-field exclusion. OpenTofu didn't ship its own ephemeral resources and write-only attributes until v1.11.0, released December 9, 2025, just over a year after Terraform 1.10. The two implementations are similar in shape, since they solve the same language-level problem, but they were built independently, so don't assume every provider-specific edge case behaves identically on both engines.
| Capability | Terraform | OpenTofu |
|---|---|---|
| Ephemeral resource blocks | v1.10 (Nov 2024) | v1.11.0 (Dec 2025) |
| Write-only arguments on managed resources | v1.11 (Feb 2025) | v1.11.0 (Dec 2025) |
| Built-in whole-file state/plan encryption | Not available in the open-source CLI; depends on the backend (for example S3 server-side encryption) or HCP Terraform's Hold Your Own Key | v1.7 (Apr 2024), via pbkdf2, AWS KMS, GCP KMS, or OpenBao key providers |
Both engines now cover the same two layers, they just built them in reverse order. If you're standardizing a platform team on one engine and used to think of this as a reason to prefer OpenTofu, the gap has narrowed: Terraform still has no first-party whole-file encryption outside HCP Terraform, but OpenTofu no longer has an exclusive claim on keeping specific secrets out of state either.
Neither feature retroactively cleans anything up. If aws_db_instance.app already has a password sitting in state from before you adopted write-only arguments, migrating the resource to password_wo doesn't erase the old value out of your state's version history, and it doesn't rotate the credential. Plan for a rotation as part of the migration, and if your backend keeps state history (S3 versioning, a platform's built-in versioning), remember old versions still hold the old plaintext value until you separately expire them.
A practical order of operations for an existing codebase:
terraform show -json | jq (or the OpenTofu equivalent) against a pulled state file, grepped for common attribute names like password, secret, token, key, tells you more than guessing from the HCL.ephemeral resource generating or fetching the value.Scalr's default managed backend stores Terraform and OpenTofu state encrypted with AES-256, and only the Scalr application holds the ability to decrypt it. Reading state at all is gated behind its own permission (state-versions:read), separate from run permissions, so giving someone the ability to trigger applies doesn't automatically give them read access to what's inside state.
Sensitive variables and provider configurations are encrypted the same way by default, under a key Scalr controls. If your compliance requirements mean you need to hold that key yourself, Scalr's Bring Your Own Key (BYOK, Enterprise) lets you supply your own AWS KMS or GCP Cloud KMS key: Scalr generates a per-account data encryption key, wraps it with your key using envelope encryption, and calls your KMS to unwrap it whenever it needs to encrypt or decrypt data. The key material itself never leaves your KMS, and revoking your key immediately cuts off Scalr's ability to decrypt that account's data. Worth being precise about scope here: BYOK protects variables, provider configurations, workspace names, and other account metadata. It doesn't cover state files, that's what storage profiles are for, and the two are designed to be used together rather than as alternatives.
For cloud credentials specifically, Scalr's provider configurations are the mechanism to reach for regardless of whether BYOK is enabled. You authenticate to AWS, Azure, or GCP once at the configuration level, then workspaces and users run against that configuration without ever seeing or handling the underlying credential themselves. Configurations are stored encrypted and never exposed in plan or apply output, the same protection a write-only argument gives a single resource, applied at the platform level to every run that uses it.
If your compliance requirements call for state to live in a bucket you control, Scalr's storage profiles (Enterprise) let you point state, plan artifacts, and logs at your own S3, GCS, or Azure Storage account, with your own KMS key, while Scalr keeps handling locking, versioning, run orchestration, and access control on top. Decide deliberately, though, rather than layering on OpenTofu's own encryption block by default: turn that on for a workspace Scalr manages and Scalr (or any platform) ends up holding ciphertext it can't parse, so anything that depends on reading state directly, drift detection against live infrastructure, a resource browser, stops working for that workspace. Pick one layer to own state protection: either encrypted storage you control, or field-level exclusion plus a platform's own encrypted backend, and apply it consistently instead of half-doing both. See our Terraform state and backends guide for how storage profiles fit alongside S3, GCS, and Azure backends, and pricing for which tier storage profiles are included in.
sensitive = true was never a security control, just a display filter, and both major engines now give you a real one. Terraform's write-only arguments and ephemeral resources keep specific secrets out of state entirely; OpenTofu's built-in encryption protects everything else still sitting in there. Use both, rotate what's already exposed, and lock down who can read state regardless of which engine you're on.
