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 default, in a .tfvars file or in a resource argument is committed with the code.

Nothing. Never write a secret in a file that is committed. Pass it through TF_VAR_ or read it from a secret manager.

The state file

Every resource argument and attribute is stored. A database password or a generated random_password result is stored in plain text in the JSON document.

Only the storage: encryption at rest, access control, versioning and audit logs of the backend, or OpenTofu state encryption. The sensitive flag does not protect it.

A saved plan file

terraform plan -out=plan.tfplan records the prior state and the proposed values, including secrets.

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, terraform output, provider error messages and debug logging with TF_LOG.

sensitive redacts plan and apply output. It does not redact terraform output -json or -raw, provider errors that echo a value, or trace logs.

The process environment and shell history

A password passed with -var on a command line is visible in the process list and in the history of the shell.

Prefer TF_VAR_ variables set by the CI system, or -var-file with a file that exists only for the run.

Provisioner and local-exec output

A command that prints a secret writes it to the log.

Pass secrets through environment and never echo them. The mechanisms described here apply to provisioners too.

Downstream readers of state

Anyone who can use terraform_remote_state, terraform state pull or the state API reads the whole document.

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.

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

sensitive = true on variables and outputs, the sensitive() function. Redacts CLI output and the HCP Terraform user interface. The value is still stored in state and plan.

Terraform 0.15

2. Do not store the value

Ephemeral variables, ephemeral outputs of child modules, ephemeral resources and write-only arguments. The value is kept out of state and plan files entirely.

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

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 variable block with sensitive = true.

  • An output block with sensitive = true.

  • The sensitive() function applied to any expression.

  • A resource attribute that the provider declares sensitive in its schema. This is why the result of a random_password or the password argument 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 sensitive_attributes list), not an encrypted form of the value.

The claim that terraform output will not print it

It will not print it in the default, human-readable form. terraform output -json and terraform output -raw print sensitive values in plain text.

"Nobody can see it in CI logs."

Plan and apply output is redacted. Provider error messages, trace logs and a local-exec provisioner that echoes the value are not.

"A sensitive variable is validated safely."

A validation block can read the variable, and an error message that interpolates it would print it. Do not include the secret in error_message.

"It protects me against someone who can read the state."

It does not. Anyone who can read the state reads every secret in it.

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

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.

variable blocks with ephemeral = true

An ephemeral variable can be derived from another ephemeral value, for example when a module passes one on.

output blocks with ephemeral = true in a child module

Not allowed in the root module (see below).

locals blocks

A local value that references an ephemeral value is itself ephemeral.

ephemeral resource blocks

An ephemeral resource may depend on another ephemeral resource.

provider configuration blocks

The standard way to give a provider a short-lived credential.

Provisioner and connection blocks

Commands run during an operation, so the value is available while they run.

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 test command 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 ephemeral block, 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
}

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
}

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

count

Creates several instances from one block. Mutually exclusive with for_each.

for_each

Accepts a list or set of strings or a map. Mutually exclusive with count.

depends_on

A list of resource references that Terraform processes first.

provider

Selects an alternate provider configuration, written <PROVIDER>.<ALIAS>. Useful for reading a secret from a vault in another region or subscription.

lifecycle

Holds precondition and postcondition blocks. A precondition is checked before the resource is opened, a postcondition after it has produced its result.

The open, renew and close lifecycle

An ephemeral resource has a lifecycle that differs from a managed resource’s create, update and destroy:

  1. 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.

  2. 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.

  3. 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."
    }
  }
}

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.

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 null for it, always. Prior state, planned state and final state all contain null for 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_wo is 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.

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

aws_db_instance

password_wo

password_wo_version

Replaces password. The version argument is required when password_wo is set. Cannot be combined with manage_master_user_password = true.

aws_secretsmanager_secret_version

secret_string_wo

secret_string_wo_version

Replaces secret_string. Required unless secret_binary or secret_string is set. The attribute has_secret_string_wo reports that a write-only value is set.

azurerm_key_vault_secret

value_wo

value_wo_version

One of value or value_wo must be specified. The version is an integer to increment when updating the value.

azurerm_postgresql_flexible_server

administrator_password_wo

administrator_password_wo_version

Either administrator_password or administrator_password_wo is required when create_mode is Default and password authentication is enabled.

google_secret_manager_secret_version

secret_data_wo

secret_data_wo_version

Only one of secret_data or secret_data_wo can be set. Increment the version to update.

google_sql_user

password_wo

password_wo_version

An integer to increment when updating the password.

vault_kv_secret_v2

data_json_wo

data_json_wo_version

Either data_json or data_json_wo is required. The value is not read back from the API.

vault_database_secret_backend_connection (PostgreSQL block)

password_wo

password_wo_version

Used in the Vault provider’s own example for the PostgreSQL connection.

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 Computed, cannot be used in a set, and cannot be defined in a data source or in a provider schema.

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
}

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
}

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
}

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}"
}

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

ephemeral resource read from the secret manager, used in the provider block

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 (secret_string_wo, value_wo, secret_data_wo, data_json_wo) and let applications read the manager

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 result is marked sensitive, which only hides it from the CLI.

No.

Stable across runs

Yes, until it is replaced (for example by changing keepers). Safe to use as an ordinary argument.

No. A new value is generated in every phase of every run.

keepers

Supported: a map whose change triggers a new password.

Not applicable: nothing persists to compare with.

Attributes

result and bcrypt_hash (sensitive), id.

result and bcrypt_hash (sensitive).

Can feed a normal argument

Yes.

No. Only write-only arguments, other ephemeral contexts or terraform_data with store.

Use when

The target resource has no write-only argument for its password.

The target has a write-only argument (password_wo and similar).

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.

Root module 1: create the database and the secret
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
}
Root module 2: configure the database with a credential read ephemerally
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"
}

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

flowchart TD A["A secret is needed in a configuration"] --> B{"Does the target resource have a write-only argument?"} B -- yes --> C["Source it from an ephemeral resource or ephemeral variable and bump the version argument"] B -- no --> D{"Is the secret only needed by a provider or provisioner?"} D -- yes --> E["Read it with an ephemeral resource into the provider block"] D -- no --> F["Use a sensitive variable or random_password and treat state as a secret store"] F --> G["Encrypt the backend, restrict reads, enable audit logs, OpenTofu state encryption"] C --> H["Store the value in a secret manager for the applications that need it"] E --> H

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

sensitive on variables and outputs

0.15.

Same behavior. OpenTofu also provides issensitive, and since 1.11 returns an unknown result for an unknown argument.

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.

ephemeralasnull

1.10.

Provided.

Ephemeral value in count or for_each

Not allowed (the instances would be stored).

Not allowed; the OpenTofu page states it explicitly and extends it to lifecycle { enabled }.

terraform.applying

Symbol, 1.10.

tofu.applying.

terraform_data store block

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 encryption block, with key providers and methods. Recommended in the OpenTofu documentation to protect values that must be stored: mark them sensitive and enable encryption.

OpenTofu state encryption is described in State and the broader comparison belongs to OpenTofu.

What changed recently

Version Change

Terraform 0.15

sensitive on variables and outputs, sensitive() and nonsensitive().

Terraform 1.10 (2024-11-27)

Ephemeral resources, ephemeral input variables and outputs, the ephemeralasnull function. Later patch releases fixed crashes when printing ephemeral values in terraform console (1.10.4).

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 test allows ephemeral outputs in root modules.

Terraform 1.16 (2026-08-26)

The store block of terraform_data. In the tests, optional ephemeral values no longer have to be set at plan time.

Terraform 1.17 (2026-10-21)

mock_provider supports ephemeral resources in tests; diagnostics raised when renewing an ephemeral resource are shown.

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 sensitive flag 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_password as 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_version and aws_secretsmanager_secret_version data 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_data or secret_string. These arguments store the secret in state; the _wo alternatives exist now.

  • A shell external data 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 sensitive removes the value from state. Check the state backend’s controls, not the flag.

  • Forgetting the version argument. Changing password_wo without changing password_wo_version changes 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 _wo suffix, or use terraform_data with store knowing 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=trace can 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.