Key Takeaway
OpenTofu 1.11, released on 9 December 2025, keeps secrets out of state and plan files with ephemeral variables, outputs and resources, plus write-only attributes that are always stored as null. Plan cannot see changes to a write-only value, so pair each one with a version attribute. The new enabled meta-argument replaces count for zero-or-one instances.
The question that sent me into the OpenTofu 1.11 documentation was a plain one: why does a database password still end up in a state file when the configuration never prints it? Marking it sensitive hides it from the terminal. It does not remove it from the file that sits in your bucket.
This post reads the primary sources closely: the 1.11 what's new page, the release announcement of 9 December 2025, and the language reference pages for ephemerality, ephemeral resources, write-only attributes, variables, outputs and the enabled meta-argument. Every rule below comes from those pages. Where the docs gave a skeleton but no full example, the HCL here is my own illustration and says so.
Sensitive hides a secret; it does not remove it
The variables reference is blunt about this. A sensitive variable has its value hidden in plan and apply output, but sensitive values are still recorded in state in cleartext, so anyone with access to the state can read them. The ephemerality page recommends ephemeral values wherever possible, and says that if data must be stored, you should mark it sensitive and enable state and plan encryption.
Ephemeral values change the storage guarantee itself. The release announcement says they exist only in memory during a single OpenTofu phase, so they are never persisted in state snapshots or plan files. Write-only attributes sit at the other end of the pipe: they carry an ephemeral value into a managed resource without leaving a copy behind. The table puts the three mechanisms side by side.
| Mechanism | In state | In plan file | CLI output |
|---|---|---|---|
| sensitive = true | Stored in cleartext | Stored (encrypt it if you need to keep it) | Shown as sensitive value |
| ephemeral variable or resource | Never stored | Never stored; only the variable name is saved | Not applicable: usable only in listed contexts |
| write-only attribute | Always null | Always null | Shown as write-only attribute |
Read the table as a ladder. Sensitive is a display setting. Ephemeral and write-only are storage rules, enforced by OpenTofu rather than by good manners. The docs still recommend state and plan encryption for anything you cannot make ephemeral, because the two features cover different values.
Ephemeral variables and outputs in practice
An input variable becomes ephemeral with one argument. OpenTofu does not store its value in state, and the plan keeps only the variable name. Anything computed from it is ephemeral too: the docs show trimspace applied to an ephemeral password yielding an ephemeral local.
variable "db_admin_password" {
type = string
ephemeral = true # never written to state; the plan keeps only the variable name
}
locals {
# Anything derived from an ephemeral value is itself ephemeral.
sanitized = trimspace(var.db_admin_password)
}
# modules/secrets/outputs.tf -- a CHILD module. A root module output block
# cannot carry ephemeral = true.
output "db_password" {
value = local.sanitized
ephemeral = true
}Outputs follow a stricter rule. An output that references an ephemeral value must itself be marked ephemeral, and the outputs reference states that the argument is for child modules only: a root module output block cannot be configured with it. That detail matters because a scouted summary of the feature said outputs can be ephemeral without that qualification. In a module tree, secrets flow upward through child outputs, never out of the root.
For a root module variable the variables page only says it needs careful handling around the var and var-file options, and points to the apply docs. My own rule follows from that: do not put the value in a tfvars file in the repository, and do not type it on a command line that lands in shell history. Read it into an environment variable instead, as the second block shows.
# Wrong: the secret is now in your shell history (and often in a tfvars file in git).
tofu apply -var="db_admin_password=hunter2"
# Right: read it silently into the environment, then apply.
# Environment variables are literal strings by default, so no escaping is needed.
read -rs TF_VAR_db_admin_password && export TF_VAR_db_admin_password
tofu applyEphemeral resources: read a secret, never store it
An ephemeral block is the read-only sibling of a data block, with one difference that matters: its result is a temporary value that OpenTofu will not store in state or plan. The language page lists three steps in its lifecycle. OpenTofu validates the configuration with the provider, opens the resource to retrieve the value, and closes it once the current phase no longer needs it. It works only with providers that offer the capability, and it is not a drop-in replacement for data or resource blocks.
The reference supports depends_on, count, for_each, provider and lifecycle, and lifecycle takes only precondition and postcondition blocks. The OpenTofu pages I read give only the skeleton, so the AWS example below is my illustration. I confirmed the ephemeral block syntax from the docs, not the AWS type or attribute names; check them in the provider documentation before you copy.
terraform {
required_version = ">= 1.11.0" # ephemeral blocks need OpenTofu 1.11 or later
}
# Skeleton confirmed from the OpenTofu docs. The AWS type and the secret_string
# attribute below are ILLUSTRATIVE: verify them in your provider's documentation.
ephemeral "aws_secretsmanager_secret_version" "db" {
secret_id = "prod/db/admin"
lifecycle {
postcondition {
condition = self.secret_string != ""
error_message = "The secret came back empty."
}
}
}
# Allowed: a provider configuration. Not allowed: count, for_each, lifecycle enabled.
provider "postgresql" {
host = "127.0.0.1"
username = "admin"
password = ephemeral.aws_secretsmanager_secret_version.db.secret_string
}If the configuration is not fully known during planning, the open is deferred to apply, and it happens only after all dependencies are satisfied. The 1.11 announcement names two real uses: time-limited AWS credentials from OpenBao, and a temporary SSH tunnel for the cyrilgdn postgresql provider.
Write-only attributes and the version companion
A write-only attribute lives only in the configuration section of a resource. It is written to state and plan as null, the provider always returns it as null, and the CLI renders it as write-only attribute. Unlike a normal attribute it may reference both regular and ephemeral values, which is the whole point: an ephemeral secret can finally flow into a managed resource.
resource "aws_secretsmanager_secret" "db" {
name = "app/db-password"
}
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
# Write-only: state and plan hold null, the CLI prints (write-only attribute).
secret_string_wo = var.db_admin_password
# Regular companion: OpenTofu cannot see a change to the line above, so it
# watches this one. Bump it (1 -> 2) to make the next apply write the new value.
secret_string_wo_version = 1
}The catch follows directly. OpenTofu cannot detect a change to a write-only value, because state holds null and the provider returns null. So providers are asked to pair each write-only attribute with a regular one. Changing the companion tells the provider to apply the new value. The docs use the AWS secret version resource, with secret_string_wo and secret_string_wo_version.
The practical consequence is that rotation is something you do on purpose. Edit the secret in the external system, then bump the version number in the configuration so the next apply writes it. If you forget the bump, plan shows no change and the resource silently keeps the old value.
Where an ephemeral value is allowed to go
Ephemeral values are marked at the source and tracked by OpenTofu. Using one anywhere outside the permitted contexts produces an error, which is the mechanism that stops a secret leaking into state by accident. The table collects the rules from the ephemeral resources, ephemerality and enabled pages.
| Where the value is used | Allowed | What the docs say |
|---|---|---|
| Ephemeral resources, variables, outputs, locals | Yes | Listed contexts; expressions derived from them stay ephemeral |
| Provider configuration | Yes | The point of a short-lived credential |
| Provisioners and connection blocks | Yes | But provisioner output is not suppressed; mark values sensitive too |
| Write-only attributes | Yes | The only managed resource arguments that accept them |
| Ordinary resource attributes | No | Using them elsewhere produces an error |
| count and for_each | No | Explicitly listed as a limitation |
| lifecycle enabled, including tofu.applying | No | The enabled page also bars ephemeral values |
Two helpers soften the rules. The ephemeralasnull function sanitises an ephemeral value for places that cannot accept one, and tofu.applying lets configuration tell the plan and apply phases apart. Validation conditions may also reference ephemeral values.
The enabled meta-argument replaces count for one instance
The other 1.11 feature in this assignment is enabled, a clearer alternative to count for a resource or module with zero or one instance. It goes inside the lifecycle block, which the what's new page says avoids conflicts with variables or arguments already called enabled. If it is true or omitted the resource behaves normally; if false it is not created and evaluates to null.
| Concern | count | lifecycle enabled |
|---|---|---|
| Typical expression | count = var.create ? 1 : 0 | lifecycle with enabled = var.create |
| Referencing the resource | aws_instance.example[0] | aws_instance.example |
| When disabled | Empty list | Evaluates to null |
| Value restrictions | Number; no ephemeral values | Boolean only: not unknown, sensitive, null or ephemeral |
| Combining | Cannot sit beside enabled | Cannot sit beside count or for_each |
# Before: count with an index on every reference.
resource "aws_instance" "example" {
count = var.create_instance ? 1 : 0
ami = "ami-12345678"
instance_type = "t3.micro"
}
output "instance_id" {
value = var.create_instance ? aws_instance.example[0].id : null
}
# After: enabled inside lifecycle. No moved block needed; state is moved for you.
resource "aws_instance" "example" {
ami = "ami-12345678"
instance_type = "t3.micro"
lifecycle {
enabled = var.create_instance # boolean, known at plan time
}
}
output "instance_id" {
# A disabled resource is null, so compare before reading an attribute.
value = aws_instance.example != null ? aws_instance.example.id : null
}Migration is cheap. According to the enabled page, switching from count to enabled lets you drop the index, and OpenTofu moves the existing state automatically, so no moved block is needed. The cost is on the read side: a disabled resource is null, and touching an attribute of null fails during plan or apply. The page suggests guarding with try or a null check, and warns that try can mask other errors, so I prefer the explicit null comparison.
The what's new page also lists a smaller 1.11 item that fits this theme: the S3 backend can now tag state objects with state_tags, and tag the lock object with lock_tags when native S3 locking through use_lockfile is on. The page cites organisation and cost tracking as the uses. It says OpenTofu may also need the s3:PutObjectTagging permission, and it gives no version note for either argument.
terraform {
backend "s3" {
bucket = "acme-tofu-state"
key = "prod/network.tfstate"
region = "ap-southeast-3"
use_lockfile = true
# New in the 1.11 line per the what's new page. You may also need
# s3:PutObjectTagging on the state object.
state_tags = {
"object:type" = "state"
"team" = "platform"
}
lock_tags = {
"object:type" = "lock"
}
}
}Where it goes wrong
None of these failures is exotic. Each is a direct reading of a rule the docs state, and each fails quietly rather than loudly.
A write-only change that you forget to version produces no diff. State holds null and the provider returns null, so OpenTofu has nothing to compare. Treat the version attribute as part of the secret rotation runbook, not as an optional extra.
- Sensitive is not a substitute. If a value cannot be ephemeral, it still lands in state in cleartext, so the docs' advice to enable state and plan encryption stands.
- Provisioner output is not suppressed for ephemeral values. A secret passed to a provisioner can still be printed unless you also mark it sensitive.
- Ephemeral values cannot drive count, for_each or lifecycle enabled. If the number of instances depends on a secret, restructure so it depends on a regular variable.
- A disabled enabled resource is null. Any output or reference that reads an attribute directly will error at plan time for the false case, so test both values of the flag.
- Compatibility is a goal, not a promise. The ephemerality page says the design is inspired by the Terraform 1.12 concept and aims for compatibility, but breaking changes may be accepted later if incompatibilities arise. Pin the version you tested.
One more limit worth stating: this is only available to providers that implement it. Ephemeral resource types and write-only arguments are provider features, so an older provider release may offer neither for the resource you care about. I did not survey which providers ship them, and I am not going to guess.
What to do on Monday
Start by measuring the problem rather than the feature. The first command in the block below searches your current state for secret-looking attribute names; it is a crude grep, not an audit, but a hit tells you which resource to convert first.
- Run the grep against tofu state pull and against a saved plan, and list every resource that stores a password, token or private key.
- Raise required_version to at least 1.11.0, and check that the provider version you use documents the ephemeral type or write-only argument you need.
- Convert one secret end to end: ephemeral variable or resource in, write-only attribute out, plus its version companion.
- Replace one count with 0 or 1 pattern with enabled, run plan on both flag values, and confirm no moved block is needed.
- Add state_tags to the S3 backend, and keep state and plan encryption on for everything that remains sensitive rather than ephemeral.
# 1. Crude but fast: which attributes in state look like secrets?
tofu state pull | grep -i -E '"[a-z_]*(password|secret|token|private_key)[a-z_]*"'
# The same check on a saved plan, which can also hold values.
tofu plan -out=tfplan
tofu show -json tfplan | grep -i -E '"[a-z_]*(password|secret|token|private_key)[a-z_]*"'
# 2. Confirm the binary before you rely on any of this.
tofu version # ephemeral blocks, write-only attributes, enabled: 1.11 or newer
# 3. After converting a resource, plan with the flag both ways.
tofu plan -var="create_instance=true"
tofu plan -var="create_instance=false"The what's new page notes that the 1.11.x line is no longer actively maintained and points to 1.13.x as the latest documentation, so plan the upgrade to a maintained release rather than stopping at 1.11.0.
Rule to carry: decide for every secret whether it needs to exist in state at all. If the answer is no, make it ephemeral on the way in, write-only on the way out, and bump the version when you rotate it. Use sensitive and encryption only for what genuinely has to be stored.
Sources (checked live on 17 October 2026)
- OpenTofu docs: what's new in 1.11
- OpenTofu docs: ephemerality overview
- OpenTofu docs: write-only attributes
- OpenTofu docs: ephemeral resources
- OpenTofu docs: the enabled meta-argument
- OpenTofu blog: OpenTofu 1.11.0 release
- OpenTofu docs: input variables
- OpenTofu docs: output values
- OpenTofu docs: S3 backend

