Sensitive Data, Ephemeral Values and Write-Only Arguments
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Almost every real infrastructure needs secrets: the master password of a database, an API token for a provider, the
private key of a certificate, the credentials that a container pulls an image with. Terraform and OpenTofu have to
handle those secrets while doing something that, by design, writes everything it learns to disk: the state file
records every attribute of every managed object, and a saved plan records the values that are about to be applied.
For years the only tools were the sensitive flag, which hides a value from the terminal, and the advice to protect the
state well. Terraform 1.10 and 1.11 added a second, much stronger approach: values that are never written anywhere in the
first place.
This page is written for Terraform 1.17 (released 2026-10-21) and OpenTofu 1.13. It explains the threat model, the
three layers of protection (hide, do not store, protect the store), the exact syntax and the restrictions of each
mechanism, how to read secrets from the four secret managers that the Bookshelf scenario uses, and how the older
random_password resource compares with its ephemeral equivalent. Provider arguments quoted here were checked against the
provider documentation published for the current major versions: AWS 6.x, AzureRM 5.x, Google 8.x, Vault 5.x and Random 3.x.
It follows Variables, Outputs and Locals and
State in the reading path, because it builds on the sensitive and ephemeral
arguments introduced there and on the description of what the state file contains. The function reference is in
Functions. Two later pages continue the topic:
Provisioners, Actions and Post-Apply
Operations, which also receives ephemeral values, and CI/CD and
Automation, which shows how credentials reach a pipeline.
All examples use placeholders: names such as bookshelf-reviews-db, addresses under example.com and empty or
obviously fake values. No example contains a real secret, and none should ever be edited to contain one. Secrets belong
in a secret manager or in TF_VAR_ environment variables set by the CI system, never in the repository.
The threat model
A threat model for a Terraform project answers one question: where can a secret be read by someone who should not read it? The answer is wider than "the state file". The table lists the places in the order in which a typical project meets them.
| Where the secret ends up | How it gets there | What protects it |
|---|---|---|
Version control |
A password written as a |
Nothing. Never write a secret in a file that is committed. Pass it through |
The state file |
Every resource argument and attribute is stored. A database |
Only the storage: encryption at rest, access control, versioning and audit logs of the backend, or OpenTofu state
encryption. The |
A saved plan file |
|
Treat the file like the state. Do not upload it as a build artifact that others can download. |
Terminal and CI logs |
Plan and apply output, |
|
The process environment and shell history |
A password passed with |
Prefer |
Provisioner and |
A command that prints a secret writes it to the log. |
Pass secrets through |
Downstream readers of state |
Anyone who can use |
Restrict read access to state as tightly as write access, and expose individual values through outputs or a dedicated store rather than the whole state. |
Source: Terraform Docs — Manage sensitive data in your configuration and Terraform Docs — Sensitive data in state.
The official documentation states the core fact in one sentence: Terraform state and plan files can contain sensitive values, and for local state the file is plain text that includes any secret values defined in the configuration. The three responses the documentation describes map onto three levels of protection, which the rest of this page follows.
| Level | Mechanism | Available since |
|---|---|---|
1. Hide the value |
|
Terraform 0.15 |
2. Do not store the value |
Ephemeral variables, ephemeral outputs of child modules, |
Terraform 1.10 (ephemeral values) and 1.11 (write-only arguments); OpenTofu 1.11 for both |
3. Protect the store |
A remote backend with encryption at rest and access control, audit logs, and OpenTofu state and plan encryption. Protects the file as a whole. |
Backend specific; OpenTofu 1.7 for encryption |
Source: Terraform Docs — Manage sensitive data in your configuration and OpenTofu Docs — Ephemerality.
The levels are cumulative, not alternatives. The best configuration keeps secrets out of state wherever the provider
allows it (level 2), marks everything that remains as sensitive (level 1), and protects the state as if it contained
secrets anyway (level 3), because some of it always will.
Level 1: sensitive values and their limits
Marking a value sensitive is the oldest mechanism and the one that is most often misunderstood. A sensitive mark is a
property that Terraform attaches to a value while it evaluates the configuration. Wherever a marked value goes, the mark
goes with it, and the user interface replaces it with (sensitive value).
Where the mark comes from
A value becomes sensitive in four ways.
-
A
variableblock withsensitive = true. -
An
outputblock withsensitive = true. -
The
sensitive()function applied to any expression. -
A resource attribute that the provider declares sensitive in its schema. This is why the
resultof arandom_passwordor thepasswordargument of a database appears as(sensitive value)without any flag in your code.
Expressions that use a sensitive value are sensitive too: an interpolation, a join, an attribute of an object that is
built from it. A for expression over a sensitive collection is sensitive as a whole. This propagation is deliberately
conservative, so a SHA-256 hash of a secret or the one harmless field of a decoded JSON document is still marked. The
nonsensitive() function removes the mark when, and only when, you know from the properties of your system that the
derived value is safe. Terraform raises an error if you call it on a value that is not sensitive, because the call would
be redundant. The functions are described in Functions.
terraform {
required_version = ">= 1.15"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
variable "reviews_api_token" {
type = string
description = "Token that reviews-api presents to the catalog-api."
sensitive = true
}
locals {
# Still sensitive: the mark propagates through the template.
auth_header = "Bearer ${var.reviews_api_token}"
# The length of a token is not a secret; the author takes responsibility.
token_length = nonsensitive(length(var.reviews_api_token))
}
output "auth_header" {
value = local.auth_header
sensitive = true # required, otherwise Terraform reports an error
}
output "token_length" {
value = local.token_length
}
If a sensitive value reaches an output that is not marked sensitive, Terraform stops with an error and asks you to add
sensitive = true. This is a protection against leaking by accident, and it is also why people sprinkle
nonsensitive() over their code until the error goes away. Doing so removes exactly the protection the error offered.
What sensitive does not do
The sensitive argument is a display control. The official pages for the variable and output blocks say the same
thing in different words, and together they list the limits.
| Claim | Reality |
|---|---|
"The value is not in the state." |
It is. Terraform stores sensitive values in the state and in saved plan files, in plain text. The state records only
which attribute paths are sensitive (the |
The claim that |
It will not print it in the default, human-readable form. |
"Nobody can see it in CI logs." |
Plan and apply output is redacted. Provider error messages, trace logs and a |
"A sensitive variable is validated safely." |
A |
"It protects me against someone who can read the state." |
It does not. Anyone who can read the state reads every secret in it. |
Source: Terraform Docs — Manage sensitive data in your configuration, Terraform Docs — output block reference and Terraform Docs — Sensitive data in state.
A frequent related mistake concerns the random_password resource. Its result is sensitive in the plan, which makes
people think it is protected. It is stored in clear text in the state, as the provider’s own documentation recommends
moving away from: the page for the managed resource tells users that when the target resource has a write-only attribute
for the password, the ephemeral variant of random_password should be used instead, "to avoid storing the password in
the plan or state file". The comparison is in random_password: resource versus ephemeral equivalent.
Reading sensitive outputs deliberately
Sometimes a human or a script needs the value. The commands that print it are explicit, so their use is visible in a review of the pipeline.
# Human-readable output hides the value
terraform output reviews_db_endpoint
# (sensitive value)
# Explicitly request the plain text, for a script that needs it
terraform output -raw reviews_db_endpoint
# JSON includes sensitive values in clear text
terraform output -json
Source: Terraform Docs — terraform output.
The commented output is illustrative. Whether an output prints (sensitive value) depends on how it is declared. Never
run these commands in a job whose log is public, and prefer an output that exposes the location of a secret (a secret
name or ARN) over one that exposes the secret itself.
Level 2: ephemeral values
An ephemeral value is available while Terraform runs and is then forgotten. It is never written to the state and never
written to a saved plan. It exists in memory for one phase of one run. Terraform 1.10 introduced the concept with three
building blocks: the ephemeral argument of variables and outputs, ephemeral resources, and the function
ephemeralasnull. Terraform 1.11 added the fourth, write-only arguments, which are the consumers. OpenTofu implements
the same design from version 1.11.
Because an ephemeral value is not stored, Terraform cannot recompute it from state during a later phase. It would have to be produced again. That is why the language restricts where an ephemeral value may be used: only in places that do not persist their inputs.
Where ephemeral values are allowed
The Terraform documentation lists the ephemeral contexts. A reference to an ephemeral value outside of one is a plan-time error, and the error message names the context that was violated.
| Context | Notes |
|---|---|
Write-only arguments of managed resources |
The consumers. They accept ephemeral and non-ephemeral values and discard them after use. |
|
An ephemeral variable can be derived from another ephemeral value, for example when a module passes one on. |
|
Not allowed in the root module (see below). |
|
A local value that references an ephemeral value is itself ephemeral. |
|
An ephemeral resource may depend on another ephemeral resource. |
|
The standard way to give a provider a short-lived credential. |
Provisioner and |
Commands run during an operation, so the value is available while they run. |
Source: Terraform Docs — Manage sensitive data in your configuration and Terraform Docs — ephemeral block reference.
The OpenTofu documentation lists the same set (ephemeral resources, variables, outputs, locals, providers, provisioners,
resource connection blocks and write-only attributes) and adds one restriction that matters in practice: an ephemeral
value cannot be used in count or for_each, because those decide which resource instances exist, and instances are
stored. The same applies to lifecycle { enabled }.
Ephemeral variables
Setting ephemeral = true on a variable block keeps the value out of state and plan. It is the right choice for a
credential that a provider needs only during the run. The variables-outputs-and-locals page describes the argument in
the context of the other variable arguments; the points that matter here are these.
-
The value can come from any of the usual sources:
-var,-var-file,TF_VAR_variables or a prompt. -
It can be combined with
sensitive = true, which also hides it in the user interface. Use both for a secret. -
It can be used only in the ephemeral contexts above. Using it in an ordinary resource argument is an error that prevents the plan.
terraform {
required_version = ">= 1.11"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
variable "reviews_db_password" {
type = string
description = "Master password of the reviews database."
ephemeral = true
sensitive = true
}
resource "aws_db_instance" "reviews" {
identifier = "bookshelf-reviews-db"
engine = "postgres"
instance_class = "db.t4g.micro"
allocated_storage = 20
username = "reviews_admin"
skip_final_snapshot = true
# Write-only: the value is sent to AWS and never stored.
password_wo = var.reviews_db_password
password_wo_version = 1
}
Run it with the secret supplied from the environment, not from a file in the repository.
export TF_VAR_reviews_db_password="<value injected by the CI system>"
terraform apply
An important consequence of the example: Terraform no longer knows the password after the apply. If you need it later,
for example to hand it to reviews-api, you must obtain it from where you stored it (the secret manager), not from the
state. Section Reading and writing secrets with a secret manager shows the full loop.
Ephemeral outputs
An output block can set ephemeral = true to pass a value between modules without writing it to state or plan. Three
rules apply.
-
It is allowed only in a child module. The documentation states that ephemeral cannot be set on outputs of the root module. (The
terraform testcommand is the exception: Terraform 1.14 allowed ephemeral outputs in root modules while running tests.) -
The value of the output must itself come from an ephemeral context.
-
The output can be referenced only from other ephemeral contexts: another module’s ephemeral output, a write-only argument, an ephemeral variable, an
ephemeralblock, provider configuration, and provisioners and their connection configuration.
A common use is a module that reads a credential from a secret manager and hands it to the root module, so the root module can configure a provider or fill a write-only argument without the value being stored.
# modules/reviews-secret/main.tf
terraform {
required_version = ">= 1.11"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
variable "secret_id" {
type = string
description = "Name or ARN of the secret that holds the database password."
}
ephemeral "aws_secretsmanager_secret_version" "db" {
secret_id = var.secret_id
}
output "db_password" {
description = "Database password; never stored in state."
value = ephemeral.aws_secretsmanager_secret_version.db.secret_string
ephemeral = true
sensitive = true
}
Source: Terraform Docs — output block reference and AWS provider — ephemeral aws_secretsmanager_secret_version.
The module is called as usual, and the root module uses module.reviews_secret.db_password only in one of the ephemeral
contexts.
module "reviews_secret" {
source = "./modules/reviews-secret"
secret_id = "bookshelf/reviews/db"
}
resource "aws_db_instance" "reviews" {
# ... other arguments as before ...
password_wo = module.reviews_secret.db_password
password_wo_version = 1
}
Source: Terraform Docs — Ephemerality.
Ephemeral local values
A locals block may hold ephemeral values, and a local that references one becomes ephemeral itself. This is handy to
shape a secret before use. The Terraform documentation shows deriving a connection string from an ephemeral secret and
using it in a provider block.
ephemeral "aws_secretsmanager_secret_version" "catalog_db" {
secret_id = "bookshelf/catalog/db"
}
locals {
# Ephemeral because it references an ephemeral resource.
catalog_db = jsondecode(ephemeral.aws_secretsmanager_secret_version.catalog_db.secret_string)
}
provider "postgresql" {
host = "catalog-db.internal.example.com"
port = 5432
username = local.catalog_db["username"]
password = local.catalog_db["password"]
sslmode = "require"
}
The PostgreSQL provider shown is a community provider (cyrilgdn/postgresql); its host, port, username,
password and sslmode provider arguments come from its own documentation, and the required_providers entry is
omitted here for brevity.
Ephemeral resources
An ephemeral resource is a resource that exists only during one operation. Terraform opens it when it needs the result, keeps it alive as long as something uses it, and closes it. It is the ephemeral counterpart of a data source: a way to read or to produce a value, but one whose attributes are never persisted. The official reference states it directly: Terraform does not store ephemeral resources in state or plan files.
The syntax mirrors a resource or data block with a different keyword. Each provider defines its own ephemeral resource
types; a provider that has none cannot be used with the block.
ephemeral "<TYPE>" "<LABEL>" {
# arguments defined by the provider
# optional meta-arguments
count = 1
depends_on = []
provider = aws.eu
lifecycle {
precondition {
condition = true
error_message = "..."
}
postcondition {
condition = true
error_message = "..."
}
}
}
The block is referenced as ephemeral.<TYPE>.<LABEL>, with the prefix ephemeral. that distinguishes it from a managed
resource of the same type. The following table lists the meta-arguments the documentation describes.
| Argument | Behavior |
|---|---|
|
Creates several instances from one block. Mutually exclusive with |
|
Accepts a list or set of strings or a map. Mutually exclusive with |
|
A list of resource references that Terraform processes first. |
|
Selects an alternate provider configuration, written |
|
Holds |
The open, renew and close lifecycle
An ephemeral resource has a lifecycle that differs from a managed resource’s create, update and destroy:
-
Open. Terraform asks the provider to open the resource when its result is needed, for example to read a secret or to generate a password. The provider returns the attributes.
-
Renew. If the work is longer than the expiry time of the remote object (a lease, a token), Terraform asks the provider to renew it.
-
Close. After the providers and resources that depend on it have finished the current phase of the run, Terraform asks the provider to close it, so a lease can be revoked or a temporary object released.
Ephemeral resources are nodes in the dependency graph, like any other resource, so a resource that uses one of their
attributes is created after the ephemeral resource has been opened. The phases of a run (plan and apply) are separate,
and an ephemeral resource is opened again in each one; a password generated by ephemeral "random_password" in the plan
is not the password used in the apply, so you must never rely on the value being the same.
Deferred opening
If an argument of the ephemeral resource is not known until apply, Terraform cannot open it during the plan. It defers
the resource to the apply, once the unknown inputs are available. The Vault provider’s documentation illustrates it with
the mount_id argument of vault_kv_secret_v2: during terraform plan the ephemeral resource depends on the
unknown mount_id of a vault_mount that does not exist yet, so the read is deferred until the apply. A consequence is
that values derived from a deferred ephemeral resource are unknown in the plan, which is also why write-only arguments
fed from one cannot be compared during a plan.
Validation with preconditions and postconditions
Because the value of a secret is only seen at run time, conditions are the place to check that it has the shape the rest
of the configuration expects. The reference shows an aws_ssm_parameter ephemeral resource with a precondition that
blocks production use unless a compliance flag is set, and a postcondition that validates the secret’s format. The
condition and the error message must never print the secret itself.
variable "environment" {
type = string
description = "Bookshelf environment name."
}
ephemeral "aws_secretsmanager_secret_version" "reviews_db" {
secret_id = "bookshelf/${var.environment}/reviews/db"
lifecycle {
precondition {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "The environment must be dev, staging or prod."
}
postcondition {
condition = can(jsondecode(self.secret_string)["password"])
error_message = "The secret must be a JSON document with a password key."
}
}
}
Source: Terraform Docs — ephemeral block reference and AWS provider — ephemeral aws_secretsmanager_secret_version.
In a postcondition the resource’s own attributes are reached through self, as in managed resources and data sources.
Do not put self.secret_string itself in the error_message.
Many instances with for_each
An ephemeral resource can have instances. The official examples create one password per environment from a set.
terraform {
required_version = ">= 1.11"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
variable "environments" {
type = set(string)
default = ["dev", "staging", "prod"]
}
ephemeral "random_password" "db" {
for_each = var.environments
length = 32
special = true
}
The set is a default here only because the names are not secrets. The generated values are not usable by themselves:
they must be consumed by write-only arguments, by a secret manager resource, or by terraform_data with a store block,
as the following sections show.
Testing configurations that use ephemeral resources
Terraform 1.17 added mock_provider support for ephemeral resources in terraform test, so a test can replace a call to
a secret manager with canned data. Also in 1.17, diagnostics raised while renewing an ephemeral resource are now
displayed; before they were lost, so warnings may appear in configurations that worked silently. The test framework is
the subject of a later page of this section.
Source: Terraform v1.17 CHANGELOG.
ephemeralasnull
An ordinary output, a normal resource argument and a for_each collection cannot accept an ephemeral value, because they
would write it to state or plan. Sometimes a structure mixes both kinds of values and you only want to use the stable
part. ephemeralasnull(value) (Terraform 1.10) returns a value of the same type in which every ephemeral part is
replaced by a non-ephemeral null and every other part is kept. Applied to an ephemeral scalar it returns null;
applied to a map or an object, it nullifies only the ephemeral entries.
terraform {
required_version = ">= 1.10"
}
variable "session_token" {
type = string
ephemeral = true
sensitive = true
}
locals {
connection = {
environment = "staging"
session_token = var.session_token # ephemeral
}
}
# A normal output: the ephemeral entry is reported as null, the rest is kept.
output "connection_summary" {
value = ephemeralasnull(local.connection)
}
The output would show environment with its value and session_token as null. Use the function to log or export the
shape of a structure, or to feed a module that must accept an ephemeral value but should not fail when it is absent. It
does not make a secret safe to use: the secret is replaced by null, not hidden. OpenTofu provides the same function.
Write-only arguments
A write-only argument is an argument of a managed resource that Terraform sends to the provider and then forgets. Terraform 1.11 introduced the mechanism. The documentation lists the properties.
-
The value is available only during the current operation and is not stored in the state or in the plan.
-
The provider uses the value to configure the object and returns
nullfor it, always. Prior state, planned state and final state all containnullfor the argument. -
Unlike an ephemeral variable or resource, a write-only argument accepts both ephemeral and non-ephemeral values. You can pass a literal, but doing so writes the secret in the code, so the usual source is an ephemeral value.
-
The argument exists only in the configuration of the resource. It cannot be read back as an attribute. A reference such as
aws_db_instance.reviews.password_wois not available. -
Providers mark write-only arguments in their documentation (the AWS pages write "Optional, Write-Only"), and the Terraform Registry lets you check whether an argument is write-only.
Source: Terraform Docs — Write-only arguments and Terraform Docs — Write-only arguments (provider development).
The _wo and _wo_version convention
Because Terraform stores null for the argument, it cannot see that you changed the value, so it can never show a
difference for it in a plan. Provider authors therefore pair each write-only argument with a normal argument that is
stored in the state, usually named with the suffix _version. The convention is: the write-only argument ends in _wo
and its companion adds _version. To rotate the secret you change the value and increment the version. The change of
the version is visible to Terraform, the plan shows an update, and the provider reads the new write-only value and applies
it.
resource "aws_db_instance" "reviews" {
# ... engine, instance class and the rest ...
password_wo = ephemeral.random_password.reviews_db.result
password_wo_version = 2 # increment to send a new password
}
The documentation is careful to say that the implementation of write-only arguments and of their version arguments is
provider specific. The provider SDK guide describes two patterns: a version attribute, as in password_wo_version, and
a keepers map as in the Random provider, where any change to the map makes the provider use the write-only value.
Never assume a _version argument exists. Read the provider page.
The write-only arguments used in the Bookshelf scenario
The table lists the arguments verified for this page. All of them are "write-only" in the provider documentation, and each is paired with the version argument shown.
| Resource | Write-only argument | Version argument | Constraints stated by the provider |
|---|---|---|---|
|
|
|
Replaces |
|
|
|
Replaces |
|
|
|
One of |
|
|
|
Either |
|
|
|
Only one of |
|
|
|
An integer to increment when updating the password. |
|
|
|
Either |
|
|
|
Used in the Vault provider’s own example for the PostgreSQL connection. |
Source: AWS provider — aws_db_instance, AWS provider — aws_secretsmanager_secret_version, AzureRM provider — azurerm_key_vault_secret, AzureRM provider — azurerm_postgresql_flexible_server, Google provider — google_secret_manager_secret_version, Google provider — google_sql_user and Vault provider — vault_kv_secret_v2.
Provider support grows with each release: the Vault and Google providers started with three write-only arguments each and added more later, and the AWS provider adds them resource by resource. The list above is a snapshot, not a catalogue. The Google provider’s guide notes that the arguments were chosen because they were already marked sensitive, and that sensitive arguments still end in state while write-only ones do not.
Detection and the preferred alternative
Terraform reports a diagnostic titled "Available Write-only Attribute Alternative" (the Terraform 1.12 release notes
mention it when they fix duplicated reports of it). The provider SDK guide describes a validator that warns
practitioners on Terraform 1.11 or later when a regular attribute such as password is set although its write-only
counterpart password_wo could be used. Treat the warning as
an instruction to migrate. OpenTofu prints (write-only attribute) for such arguments in plan and apply output.
Limits of write-only arguments
| Limit | Consequence |
|---|---|
No drift detection |
Terraform cannot see the remote value, so it cannot tell that someone changed the password in the console. Only the version argument triggers an update. |
No outputs |
The value is not available after the apply. If another consumer needs it, store it in a secret manager first. |
Always sent |
Terraform sends the configured value to the provider on every operation, which means the ephemeral source (the secret manager) must be reachable on every plan and apply, not only when you rotate. |
Provider-defined |
Only the arguments that providers have implemented are write-only. A provider can apply the restrictions of its SDK:
for example a write-only argument cannot be |
Not a way to bootstrap |
A generated ephemeral password is different in each phase. Combine it with a secret manager resource so the value that was applied is also stored (see the next section). |
Reading and writing secrets with a secret manager
In production the credential should be stored in a secret manager, produced once, and read back whenever it is needed. The ephemeral resources and write-only arguments fit that loop exactly: an ephemeral resource reads from the manager, and a write-only argument writes to the target (a database or the manager itself) without Terraform remembering the value. The four sections apply the loop to the managers described in the cloud sections of this site (see AWS Secrets Manager and KMS, Azure Key Vault and Secrets and Google Cloud Secret Manager and KMS). For secrets that run inside a Kubernetes cluster, see Kubernetes Secrets.
AWS Secrets Manager
The AWS provider has an ephemeral resource, aws_secretsmanager_secret_version, which retrieves a secret version
including its value. The arguments are secret_id (required; the ARN or the friendly name), version_id, version_stage
(default AWSCURRENT) and region. Its attributes include secret_string, secret_binary, arn, version_id,
version_stages and created_date. A second ephemeral resource, aws_secretsmanager_random_password, generates a password
with the Secrets Manager service and exports it as random_password; its arguments include password_length,
exclude_characters, exclude_punctuation and require_each_included_type.
The complete loop for the Bookshelf reviews database generates a password ephemerally, writes it to the secret with
secret_string_wo, and applies the same value to the database with password_wo. Neither value reaches the state.
terraform {
required_version = ">= 1.11"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
ephemeral "random_password" "reviews_db" {
length = 32
special = false
}
resource "aws_secretsmanager_secret" "reviews_db" {
name = "bookshelf/reviews/db"
}
resource "aws_secretsmanager_secret_version" "reviews_db" {
secret_id = aws_secretsmanager_secret.reviews_db.id
secret_string_wo = ephemeral.random_password.reviews_db.result
secret_string_wo_version = 1 # increment to rotate
}
resource "aws_db_instance" "reviews" {
identifier = "bookshelf-reviews-db"
engine = "postgres"
instance_class = "db.t4g.micro"
allocated_storage = 20
username = "reviews_admin"
skip_final_snapshot = true
password_wo = ephemeral.random_password.reviews_db.result
password_wo_version = 1 # keep in step with the secret version
}
Source: Terraform Docs — Write-only arguments, AWS provider — aws_secretsmanager_secret_version and Random provider — ephemeral random_password.
There is a subtlety the example shows. The ephemeral password is generated again in each phase, and the plan itself does not store it. The two write-only arguments receive the value generated in the apply phase, so they agree with each other. They are only sent when their version arguments change, so after the first apply a later apply that does not change the versions sends nothing and leaves the stored secret untouched. To rotate, increment both versions in the same commit so that the secret and the database receive the same new password.
An application then reads the secret at run time (for example with the AWS SDK, or the Kubernetes External Secrets operator), and a later Terraform run that needs the value again, for example to configure a provider, reads it with the ephemeral resource of the previous section.
For RDS there is also a managed alternative that avoids the question: manage_master_user_password = true lets RDS
create and rotate the master password in Secrets Manager. It cannot be combined with password or password_wo.
Azure Key Vault
The AzureRM provider has the ephemeral resource azurerm_key_vault_secret, which requires Terraform 1.10 or later. It
takes name and key_vault_id (both required) and an optional version (default: the current version), and exports
value, expiration_date and not_before_date. The vault must be in the same subscription as the provider; for another
subscription, configure an aliased provider. The managed resource azurerm_key_vault_secret has value_wo and
value_wo_version.
The Terraform documentation shows the loop for a MySQL flexible server; the Bookshelf variant below uses the PostgreSQL
flexible server that catalog-api needs.
terraform {
required_version = ">= 1.11"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
provider "azurerm" {
features {}
}
data "azurerm_key_vault" "bookshelf" {
name = "bookshelf-kv-example"
resource_group_name = "bookshelf-rg"
}
ephemeral "random_password" "catalog_db" {
length = 32
special = true
}
resource "azurerm_key_vault_secret" "catalog_db" {
name = "catalog-db-password"
key_vault_id = data.azurerm_key_vault.bookshelf.id
value_wo = ephemeral.random_password.catalog_db.result
value_wo_version = 1
}
ephemeral "azurerm_key_vault_secret" "catalog_db" {
name = azurerm_key_vault_secret.catalog_db.name
key_vault_id = data.azurerm_key_vault.bookshelf.id
}
resource "azurerm_postgresql_flexible_server" "catalog" {
name = "bookshelf-catalog-pg-example"
resource_group_name = "bookshelf-rg"
location = "westeurope"
version = "16"
sku_name = "B_Standard_B1ms"
administrator_login = "catalog_admin"
administrator_password_wo = ephemeral.azurerm_key_vault_secret.catalog_db.value
administrator_password_wo_version = 1
}
Source: Terraform Docs — Write-only arguments, AzureRM provider — ephemeral azurerm_key_vault_secret and AzureRM provider — azurerm_key_vault_secret.
The azurerm_postgresql_flexible_server arguments version, sku_name, administrator_login and location are
shown with the values from the provider’s own examples; the other arguments the resource needs for a real deployment
(network configuration, storage, backups) are omitted. The ephemeral secret read is placed after the managed secret
because it uses its name, which orders the graph.
Google Cloud Secret Manager
The Google provider has the ephemeral resource google_secret_manager_secret_version. Its arguments are secret
(required), project, version and is_secret_data_base64; the documented attributes are secret_data, name,
create_time and destroy_time. The managed resource google_secret_manager_secret_version has the write-only argument
secret_data_wo with secret_data_wo_version, and the provider’s own example passes the ephemeral value to a resource
with a write-only argument, google_monitoring_uptime_check_config.
terraform {
required_version = ">= 1.11"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 8.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
variable "project_id" {
type = string
description = "Google Cloud project that hosts Bookshelf."
}
ephemeral "random_password" "web_db" {
length = 32
special = false
}
resource "google_secret_manager_secret" "web_db" {
project = var.project_id
secret_id = "bookshelf-web-db-password"
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "web_db" {
secret = google_secret_manager_secret.web_db.id
secret_data_wo = ephemeral.random_password.web_db.result
secret_data_wo_version = 1
}
resource "google_sql_database_instance" "web" {
name = "bookshelf-web-sql-example"
project = var.project_id
region = "europe-west1"
database_version = "POSTGRES_16"
deletion_protection = false
settings {
tier = "db-f1-micro"
}
}
resource "google_sql_user" "web" {
name = "web"
project = var.project_id
instance = google_sql_database_instance.web.name
password_wo = ephemeral.random_password.web_db.result
password_wo_version = 1
}
Source: Google provider — google_secret_manager_secret_version, Google provider — Use write-only arguments and Google provider — google_sql_user.
The replication { auto {} } block declares automatic replication for the secret. To read the secret later in a different configuration, use
the ephemeral resource:
ephemeral "google_secret_manager_secret_version" "web_db" {
project = var.project_id
secret = "bookshelf-web-db-password"
version = "latest"
}
HashiCorp Vault
The Vault provider added two ephemeral resources together with write-only arguments in its 5.x series:
vault_kv_secret_v2 and vault_database_secret. The ephemeral vault_kv_secret_v2 takes mount and name (both
required) and optional version and mount_id; it exports data (a map of the top-level keys; non-string values are
serialized as JSON), data_json, created_time, custom_metadata, deletion_time and destroyed. Setting mount_id
to the ID of the vault_mount resource defers the read to the apply, as the guide for the provider explains. The
managed vault_kv_secret_v2 has the write-only data_json_wo with data_json_wo_version.
terraform {
required_version = ">= 1.11"
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 5.0"
}
}
}
resource "vault_mount" "bookshelf" {
path = "bookshelf-kv"
type = "kv"
options = { version = "2" }
}
# Written once with a write-only argument: not stored in state.
resource "vault_kv_secret_v2" "reviews_db" {
mount = vault_mount.bookshelf.path
name = "reviews-db"
data_json_wo = jsonencode({
password = "<generated or supplied outside the repository>"
})
data_json_wo_version = 1
}
# Read ephemerally wherever it is needed.
ephemeral "vault_kv_secret_v2" "reviews_db" {
mount = vault_mount.bookshelf.path
mount_id = vault_mount.bookshelf.id
name = vault_kv_secret_v2.reviews_db.name
}
output "reviews_db_secret_path" {
description = "Where the password is stored, not the password."
value = "${vault_mount.bookshelf.path}/${vault_kv_secret_v2.reviews_db.name}"
}
Source: Vault provider — ephemeral vault_kv_secret_v2, Vault provider — vault_kv_secret_v2 and Vault provider — Use write-only attributes.
The data_json_wo value in the example is deliberately a placeholder: in practice it comes from an ephemeral resource, an
ephemeral variable or an out-of-band process, never from a literal in the code. The Vault provider’s own example for the
PostgreSQL connection of the database secrets engine sets password_wo and password_wo_version inside the
postgresql block of vault_database_secret_backend_connection, with the password read from the ephemeral
vault_kv_secret_v2. The Vault provider itself has to authenticate to Vault, so prefer short-lived
authentication over a static token, for the same reasons as in the rest of this page.
Choosing what to read, and where
| Need | Pattern | Why |
|---|---|---|
Provider credential that is needed only during the run |
|
Never stored, short-lived, nothing to rotate in state. |
Password given to a database or service Terraform creates |
Ephemeral generator or reader into a write-only argument, plus a version argument |
The secret never enters state; rotation is a version bump. |
Password that applications must read later |
Write it to a secret manager with its write-only argument ( |
One source of truth, access governed by the manager’s policies. |
Resource that has no write-only argument |
Keep using a normal argument and a sensitive variable, and protect the state (level 3) |
There is no alternative until the provider adds one. |
random_password: resource versus ephemeral equivalent
The Random provider offers random_password twice: as a managed resource and as an ephemeral resource. The two have
almost the same arguments, and they differ in what is stored.
| Property | Managed resource random_password |
Ephemeral resource random_password |
|---|---|---|
Stored in state and plan |
Yes, in clear text. The |
No. |
Stable across runs |
Yes, until it is replaced (for example by changing |
No. A new value is generated in every phase of every run. |
|
Supported: a map whose change triggers a new password. |
Not applicable: nothing persists to compare with. |
Attributes |
|
|
Can feed a normal argument |
Yes. |
No. Only write-only arguments, other ephemeral contexts or |
Use when |
The target resource has no write-only argument for its password. |
The target has a write-only argument ( |
The arguments shared by both are length (required), lower, upper, numeric, special, min_lower, min_upper,
min_numeric, min_special and override_special. The managed resource also has a number argument, deprecated in
favor of numeric. The provider’s documentation for each variant points to the other: use the ephemeral one when the
target has a write-only attribute for the password, and the managed one otherwise.
A consequence worth stating: because the ephemeral value changes on every run, a configuration that passes it to a write-only argument without a version argument would send a new password on each apply if the provider honored it silently. That is exactly why the version arguments exist. Always set the version and change it deliberately.
The terraform_data store block
Terraform 1.16 (released 2026-08-26) added a store block to the built-in terraform_data resource. According to the
release notes, it "can hold ephemeral and sensitive values across plan and apply". Its arguments are input (write-only,
accepts ephemeral values), sensitive, version and replace; the stored value is exposed as the attribute
store.output, or as store.sensitive_output when sensitive is set. The resource page describes three examples,
including persisting an ephemeral value: an ephemeral random_password result written to a store block with
sensitive = true and version = 1, exposed through a sensitive output.
The key point for this page is the one the resource page makes: the value is saved in state, through output or
sensitive_output. terraform_data with store is a way to carry an ephemeral value from one phase to another, for
example when a later resource needs the same generated value that was applied earlier. It is not a way to keep a secret
out of state. The store block is documented in detail in
Resources and Data Sources.
terraform {
required_version = ">= 1.16"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
ephemeral "random_password" "redis" {
length = 24
special = false
}
resource "terraform_data" "redis_password" {
store {
input = ephemeral.random_password.redis.result
sensitive = true
version = 1 # a change to input is ignored until this changes
}
}
output "redis_password_for_consumers" {
value = terraform_data.redis_password.store.sensitive_output
sensitive = true
}
Choose it only when an ordinary resource needs the value and no write-only argument exists. Treat the output as exposing
the secret: whoever reads the state reads the password. Under OpenTofu, the store block was not found in the OpenTofu
documentation or source; see the differences below.
A complete Bookshelf example
The example below combines the layers for the AWS flavor of Bookshelf. catalog-api and reviews-api share a
PostgreSQL instance. The master password is produced ephemerally, written once to Secrets Manager and to the database
through write-only arguments, and read back by a second root module, which configures the PostgreSQL provider.
terraform {
required_version = ">= 1.11"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.9"
}
}
}
variable "password_version" {
type = number
description = "Increment to rotate the database master password."
default = 1
}
ephemeral "random_password" "db" {
length = 32
special = false
}
resource "aws_secretsmanager_secret" "db" {
name = "bookshelf/shared/db"
}
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string_wo = ephemeral.random_password.db.result
secret_string_wo_version = var.password_version
}
resource "aws_db_instance" "shared" {
identifier = "bookshelf-shared-db"
engine = "postgres"
instance_class = "db.t4g.small"
allocated_storage = 20
username = "bookshelf_admin"
skip_final_snapshot = true
password_wo = ephemeral.random_password.db.result
password_wo_version = var.password_version
}
output "db_secret_id" {
description = "Secret that holds the master password. The password itself is not an output."
value = aws_secretsmanager_secret.db.id
}
output "db_address" {
value = aws_db_instance.shared.address
}
terraform {
required_version = ">= 1.11"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
postgresql = {
source = "cyrilgdn/postgresql"
version = "~> 1.0"
}
}
}
variable "db_secret_id" {
type = string
}
variable "db_address" {
type = string
}
ephemeral "aws_secretsmanager_secret_version" "db" {
secret_id = var.db_secret_id
}
provider "postgresql" {
host = var.db_address
port = 5432
username = "bookshelf_admin"
password = ephemeral.aws_secretsmanager_secret_version.db.secret_string
sslmode = "require"
}
resource "postgresql_database" "catalog" {
name = "catalog"
}
resource "postgresql_database" "reviews" {
name = "reviews"
}
Source: Terraform Docs — Ephemerality, AWS provider — aws_db_instance and PostgreSQL provider — configuration.
In this layout, nothing in either state contains the master password. The state of the first module records the secret’s
name and the database’s address; the state of the second records the databases. The pipeline passes the two outputs from
the first to the second through variables (for example from terraform output -raw in the CI job). For the pipeline side,
including OIDC federation so that no cloud keys are stored either, see
CI/CD and Automation.
What this design does not protect: the AWS provider’s own state for aws_db_instance still holds attributes
the provider chooses to read back (hostnames, ARNs and similar, which are not secrets), and anyone with permission to
read the secret in Secrets Manager reads the password. Those permissions are the actual access control now, so they
deserve the same review as the state backend’s.
A decision flow
The diagram is a summary of the rules above and of the reading order in the official documentation: keep the secret out of storage where possible, hide it otherwise, and protect the store in every case.
Terraform and OpenTofu differences
| Topic | Terraform | OpenTofu |
|---|---|---|
|
0.15. |
Same behavior. OpenTofu also provides |
Ephemeral variables, outputs, resources and locals |
1.10. |
1.11. The documentation says the design is "inspired by and aims for compatibility with" Terraform, and that the team may make breaking changes later if incompatibilities appear. |
Write-only arguments |
1.11. |
1.11 ("Write-only attributes can be used only with OpenTofu v1.11 onwards"). They always appear in plan and apply output as "(write-only attribute)" and are always null in state and plan. |
|
1.10. |
Provided. |
Ephemeral value in |
Not allowed (the instances would be stored). |
Not allowed; the OpenTofu page states it explicitly and extends it to |
|
Symbol, 1.10. |
|
|
1.16. |
Not found in the OpenTofu documentation or source (main branch) at the time of writing. |
Encryption of state and plan |
Not available in the CLI. Use a backend with encryption at rest. |
1.7: the |
What changed recently
| Version | Change |
|---|---|
Terraform 0.15 |
|
Terraform 1.10 (2024-11-27) |
Ephemeral resources, ephemeral input variables and outputs, the |
Terraform 1.11 (2025-02-27) |
Write-only attributes on resources. 1.11.4 fixed internal providers to set write-only attributes to null. |
Terraform 1.12 (2025-05-14) |
Duplicate attribute-associated diagnostics such as "Available Write-only Attribute Alternative" are no longer reported twice. 1.12.2 fixed partial ephemeral values being rejected in ephemeral outputs. |
Terraform 1.13 |
Variable validation keeps sensitive and ephemeral metadata when evaluating conditions (1.13.3). Sensitive inputs are hidden when values changed between plan and apply (1.13.2). |
Terraform 1.14 |
|
Terraform 1.16 (2026-08-26) |
The |
Terraform 1.17 (2026-10-21) |
|
OpenTofu 1.11 |
Ephemeral resources, ephemeral variables and write-only attributes. |
Where the books are outdated or wrong
The books that this section draws on predate ephemeral values and write-only arguments, so their advice on secrets is a good description of level 1 and level 3 and an incomplete one of level 2.
-
"Mark it sensitive and it is safe." The
sensitiveflag appears in the variables chapters as the answer to secrets. It hides output only; the value stays in state and plan. One book’s chapter on secrets lists the anti-patterns correctly (hard-coding,tfvars, state) but treats state storage as the only remedy. -
"Secrets in state are unavoidable." Not any more. Ephemeral resources (Terraform 1.10) and write-only arguments (1.11) keep a growing set of secrets out of state altogether, subject to provider support.
-
random_passwordas the standard generator. It is still valid, but since write-only arguments it is the fallback for resources without one; the provider’s own documentation recommends the ephemeral variant where possible. -
Reading secrets with a data source. The
google_secret_manager_secret_versionandaws_secretsmanager_secret_versiondata sources, shown in older material, store the value in the state. The ephemeral resources with the same names do not. -
Cloud secret creation with
secret_dataorsecret_string. These arguments store the secret in state; the_woalternatives exist now. -
A shell
externaldata source to fetch secrets. It has the same problem: its result is stored. Prefer an ephemeral resource. -
The Wang chapter’s secret version lifecycle (
ttl,expire_time,create_before_destroy). Those arguments still apply to the managed secret, but the version-argument pattern replaces most of the rotation tricks.
Pitfalls
-
Using
nonsensitive()to silence an error. It removes the one protection Terraform offered. Fix the output declaration instead. -
Believing
sensitiveremoves the value from state. Check the state backend’s controls, not the flag. -
Forgetting the version argument. Changing
password_wowithout changingpassword_wo_versionchanges nothing: the plan shows no difference, and the provider is never told. -
Rotating only half. When the same ephemeral value feeds a secret manager and a database, bump both versions together. Bumping one leaves the secret and the database with different passwords.
-
Expecting the same ephemeral value in plan and apply. It is produced again. Do not compare values between phases.
-
Passing an ephemeral value to an ordinary argument. It is a plan-time error. Look for the write-only alternative with the
_wosuffix, or useterraform_datawithstoreknowing the value will be stored. -
Outputs. Ephemeral outputs exist only in child modules (and in tests); a root module cannot export an ephemeral value, by design. Export the secret’s location, not its value.
-
Reading secrets from a data source out of habit. Use the ephemeral resource if the provider has one.
-
Unreachable secret manager. Write-only and ephemeral inputs are read on every plan and apply. Make the manager available to the pipeline, and grant it the minimum permissions, as for any other dependency.
-
Debug logging.
TF_LOG=tracecan print values in provider requests. Do not enable it in shared logs. -
Plan files. Level 2 keeps secrets out of plan files, but any remaining sensitive value is still in them. Treat a saved plan like the state.