TrademarkTrademark
Features
Documentation

Secrets in Terraform State: Why They Leak, and the Fix

Terraform and OpenTofu both leak secrets into state by default. Learn how ephemeral values, write-only arguments, and state encryption close the gap.
Ryan FeeJuly 6, 2026
Key takeaways
  • Marking a variable sensitive = true only hides it from CLI output. Terraform and OpenTofu still write the real value into the state file and any saved plan file, in plaintext, by default.
  • Terraform added ephemeral resource blocks in v1.10 (November 2024) and write-only arguments in v1.11 (February 2025) so a provider can use a secret during an operation without Terraform ever storing it.
  • OpenTofu shipped built-in, whole-file state encryption (PBKDF2, AWS KMS, GCP KMS, OpenBao key providers) in v1.7, April 2024, a feature the open-source Terraform CLI still doesn't have natively.
  • OpenTofu didn't get ephemeral resources and write-only attributes until v1.11.0 in December 2025, just over a year after Terraform shipped the same capability in 1.10.
  • The two approaches attack different layers: write-only arguments keep specific fields out of state entirely, while state encryption protects everything else that's still in there. Most teams need both, not one or the other.

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.

Why sensitive = true doesn't do what people think

variable "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's fix: keep the secret out of state in the first place

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:

  • Provider support is opt-in and still spreading. A resource only gets write-only arguments when its provider implements them, and only for the specific attributes the provider maintainer chose to expose. Check the resource's page on the registry; _wo and _wo_version suffixes are the convention.
  • Write-only arguments accept ephemeral or plain values. You can pass a literal string to a write-only argument if you want, though that defeats most of the purpose. Pairing it with an ephemeral resource, or with an ephemeral read from a secrets manager, is the actual point.

OpenTofu's fix: encrypt the whole file, then catch up on the rest

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.

Terraform vs. OpenTofu: two different layers, now roughly at parity

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.

What this means for your actual state file today

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:

  1. Inventory what's actually in state. 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.
  2. Move new and rotated secrets to write-only arguments where the provider supports them, paired with an ephemeral resource generating or fetching the value.
  3. Rotate the credentials you find in step 1, since moving to a write-only argument going forward doesn't clean the old value out of state history.
  4. Encrypt the state file at rest as a second layer, whichever engine you're on, for everything that isn't or can't be a write-only argument, IP addresses, ARNs, and every other attribute Terraform legitimately needs to keep tracking.
  5. Restrict who can read state at all. Encryption and write-only arguments both reduce what's exposed if state leaks; access control reduces how often it's exposed to begin with.

Where Scalr fits

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.

The short version

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.

Frequently asked questions

Does marking a Terraform variable as sensitive keep it out of state?

No. The sensitive = true flag only suppresses the value from CLI output and logs. Terraform and OpenTofu still write the real, unencrypted value into the state file and into any saved plan file. If you need a secret to never be persisted, use a write-only argument or an ephemeral resource instead of relying on sensitive = true.

What Terraform version added ephemeral values and write-only arguments?

Ephemeral resource blocks shipped in Terraform 1.10 (November 2024). Write-only arguments, which let a managed resource accept an ephemeral value without storing it, require Terraform 1.11 or later (February 2025) and a provider that has implemented the specific write-only attribute.

Does OpenTofu support ephemeral resources and write-only arguments?

Yes, as of OpenTofu 1.11.0, released December 9, 2025, just over a year after Terraform shipped the same capability in 1.10.

Can OpenTofu encrypt the entire state file, not just specific fields?

Yes. OpenTofu has shipped built-in state and plan encryption since v1.7 (April 2024), using a key_provider and method block in configuration. Supported key providers include PBKDF2 (passphrase-based), AWS KMS, GCP KMS, and OpenBao. Terraform's open-source CLI has no equivalent native feature; encryption depends on the backend (for example S3 server-side encryption) or, for HCP Terraform customers, the Hold Your Own Key add-on.

If I encrypt Terraform or OpenTofu state, can my IaC platform still read it?

Not if the platform doesn't hold the decryption key. OpenTofu's encryption block runs client-side before state ever leaves the OpenTofu process, so a platform storing that state holds ciphertext it can't parse. Features that read state directly, such as drift comparison against live infrastructure or a resource browser, stop working on an encrypted workspace unless the platform is also given the key.

What's the difference between write-only arguments and ephemeral resources?

An ephemeral resource block produces a value, such as a generated password or a short-lived credential, that only exists in memory during the current operation. A write-only argument is where that value goes: a special resource attribute that accepts the ephemeral value, hands it to the provider, and is then discarded rather than written to state. You typically use them together, an ephemeral resource generating the secret and a write-only argument accepting it.

Does Scalr's Bring Your Own Key (BYOK) feature encrypt Terraform state?

No. BYOK (Enterprise) lets you supply your own AWS KMS or GCP Cloud KMS key so Scalr encrypts variables, provider configurations, workspace names, and other account metadata under a key you control instead of one Scalr controls. It doesn't apply to Terraform or OpenTofu state files. If you need to own how state itself is encrypted and stored, use storage profiles, which are designed to be used alongside BYOK rather than in place of it.
About the author
Ryan Feedirector of platform engineering at Scalr
Ryan Fee is the director of platform engineering at Scalr, with over 15 years of experience improving infrastructure experiences at companies large and small.