Handling Sensitive and Ephemeral Data in Terraform
Contents
Introduction
Infrastructure as Code (IaC) tools like Terraform bring automation, scalability, and repeatability to infrastructure
provisioning. However, they also introduce new challenges in managing sensitive data, such as API keys, passwords,
private keys, tokens, and other secrets. Mishandling these values throughout the deployment lifecycle can result
in unintended exposure via CLI output, Terraform state files, logs, or version control systems.
Terraform provides built-in features to mitigate these risks, including the sensitive attribute to suppress secret
values from output and the newer ephemeral attribute for handling transient data that shouldn't be stored in state.
In addition, certain resource arguments are treated as write-only, ensuring the values cannot be read back once applied.
Cloud providers such as GCP, AWS, and Azure also support ephemeral resources that are intended for temporary
or one-time use.
This tutorial explores the key Terraform features that help managing sensitive and ephemeral data effectively.
You'll learn how to:
- Use the
sensitive
attribute in variables and outputs
- Understand the role of
ephemeral
attributes and resources
- Work with write-only arguments supported by various providers
- Protect sensitive information in Terraform state and output
- Apply best practices for secure module design and secret management
Whether you're building secure infrastructure for production environments or managing secrets in development workflows,
this guide will help you protect your data and reduce risk.
Let's dive in!
Where Sensitive Data Can Be Exposed
In some situation, it may be required to include and manage sensitive data such as passwords, API tokens, private keys,
and secrets within your Terraform code. However, unless properly handled, these values can be unintentionally exposed
in Terraform state files, plan or apply outputs, or log files.
Even if Terraform recognizes some resource attributes as sensitive and masks them as (sensitive value)
in the plan and apply outputs, such attributes may still be stored in plaintext in the Terraform state file.
To illustrate, let's review the following GCP Secret Manager configuration:
# main.tf
variable "project" {
type = string
description = "GCP project to create secret in"
}
variable "api_token" {
type = string
# run: export TF_VAR_api_token=abcd1234-very-secret-token
}
resource "google_secret_manager_secret" "secret_token" {
secret_id = "api-token"
project = var.project
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "secret_token_version" {
secret = google_secret_manager_secret.secret_token.id
secret_data = var.api_token
}
output "api_token" {
value = var.api_token
}
This Terraform configuration creates a secret in Secret Manager with the ID api-token
using the
google_secret_manager_secret
resource. It then creates a new version for that secret, populating
it with the value from the var.api_token
variable using the google_secret_manager_secret_version
resource. In addition, the code defines an output named api_token
that displays the value of the secret.
Terraform Plan Output:
Let's assign a value to the api_token
input using the TF_VAR_api_token
environment variable and
run terraform plan
:
$ export TF_VAR_api_token=abcd1234-very-secret-token
$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# google_secret_manager_secret.secret_token will be created
+ resource "google_secret_manager_secret" "secret_token" {
...
}
# google_secret_manager_secret_version.secret_token_version will be created
+ resource "google_secret_manager_secret_version" "secret_token_version" {
+ create_time = (known after apply)
+ deletion_policy = "DELETE"
+ destroy_time = (known after apply)
+ enabled = true
+ id = (known after apply)
+ is_secret_data_base64 = false
+ name = (known after apply)
+ secret = (known after apply)
+ secret_data = (sensitive value)
+ secret_data_wo = (write-only attribute)
+ secret_data_wo_version = 0
+ version = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ api_token = "abcd1234-very-secret-token"
...
As we can see, Terraform masks the sensitive attribute secret_data
, but outputs the
api_token
value in clear text.
Terraform Apply Output:
$ terraform apply -auto-approve
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# google_secret_manager_secret.secret_token will be created
+ resource "google_secret_manager_secret" "secret_token" {
...
}
# google_secret_manager_secret_version.secret_token_version will be created
+ resource "google_secret_manager_secret_version" "secret_token_version" {
+ create_time = (known after apply)
+ deletion_policy = "DELETE"
+ destroy_time = (known after apply)
+ enabled = true
+ id = (known after apply)
+ is_secret_data_base64 = false
+ name = (known after apply)
+ secret = (known after apply)
+ secret_data = (sensitive value)
+ secret_data_wo = (write-only attribute)
+ secret_data_wo_version = 0
+ version = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ api_token = "abcd1234-very-secret-token"
google_secret_manager_secret.secret_token: Creating...
google_secret_manager_secret.secret_token: Creation complete after 1s [id=projects/project-ID/secrets/api-token]
google_secret_manager_secret_version.secret_token_version: Creating...
google_secret_manager_secret_version.secret_token_version: Creation complete after 1s [id=projects/project-num/secrets/api-token/versions/1]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
api_token = "abcd1234-very-secret-token"
Again, Terraform masks the sensitive attribute secret_data
, and outputs the
api_token
value in clear text.
Terraform Console:
$ terraform console
> var.api_token
"abcd1234-very-secret-token"
> google_secret_manager_secret_version.secret_token_version.secret_data
(sensitive value)
>
Terraform State File:
$ cat terraform.tfstate
{
"version": 4,
"terraform_version": "1.11.2",
"serial": 44,
"lineage": "01941147-37df-4f39-a5ec-c0689f7b2229",
"outputs": {
"api_token": {
"value": "abcd1234-very-secret-token",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "google_secret_manager_secret",
"name": "secret_token",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"instances": [
...
]
},
{
"mode": "managed",
"type": "google_secret_manager_secret_version",
"name": "secret_token_version",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"create_time": "2025-08-06T11:47:39.401190Z",
"deletion_policy": "DELETE",
"destroy_time": "",
"enabled": true,
"id": "projects/project-num/secrets/api-token/versions/1",
"is_secret_data_base64": false,
"name": "projects/project-num/secrets/api-token/versions/1",
"secret": "projects/project-ID/secrets/api-token",
"secret_data": "abcd1234-very-secret-token",
"secret_data_wo": null,
"secret_data_wo_version": 0,
"timeouts": null,
"version": "1"
},
"sensitive_attributes": [
[
{
"type": "get_attr",
"value": "secret_data"
}
]
],
"private": "eyJlMmJmYjcz...IwMDAwMDAwMDAwMH19",
"dependencies": [
"google_secret_manager_secret.secret_token"
]
}
]
}
],
"check_results": null
}
As we can see, there is a significant security issue with this configuration. The api_token
value
is displayed in plain text on the console during plan and apply operations, and stored unencrypted in the
Terraform state file.
Other potential data exposure points highlighted by this example are the local shell history and various operating
system log files. When Terraform variables are passed via the TF_VAR_*
environment mechanism
(e.g., export TF_VAR_api_token=abcd1234-very-secret-token
) or directly on the command line
(e.g., terraform apply -var="api_token=abcd1234-very-secret-token"
), the secret values
may be saved in the shell history (.bash_history
, .zsh_history
, etc.), making them
accessible to any user with access to the file. Similarly, if Terraform is invoked from scripts, CI/CD systems,
or wrapped by automation tools, secrets can unintentionally appear in system logs, audit logs, or process
listings (ps aux
, systemd journal, etc.).
In the following sections we will demonstrate how to use Terraform's sensitive
attribute together with
the ephemeral and write-only mechanisms to address these security issues.
Back to Top
The sensitive = true
Attribute
Terraform provides the sensitive
attribute as a built-in mechanism to mark variables and
outputs as sensitive and suppress the display of such values in command-line output of terraform plan
and terraform apply
.
This attribute helps reduce accidental exposure of secrets like passwords, tokens, or private keys in terminal
sessions or CI/CD pipeline logs. However, it's important to note that sensitive = true
does not encrypt
the data or prevent it from being stored in Terraform state files, where it remains in plaintext.
Where sensitive = true
Can Be Used
variable
block - Marks input variables as sensitive
locals
block - Marks local variables as sensitive. Inherits sensitivity from inputs
output
block - Hides output values unless explicitly accessed
How sensitive = true
Works
- When a value is marked as sensitive, Terraform:
- Propagates the sensitivity to any dependent outputs or expressions
- Masks the value in CLI outputs
- Requires explicit
terraform output
commands to reveal it
- It does not:
- Prevent use of the value in interpolation or expressions
- Encrypt the value in the state file
Code Example with sensitive = true
:
# main.tf
variable "short_password" {
type = string
sensitive = true
}
locals {
long_password = "secret${var.short_password}"
}
output "password" {
value = local.long_password
sensitive = true
}
The variable short_password
is defined with sensitive = true
, therefore Terraform will
treat it as sensitive. In addition, since var.short_password
is sensitive, local.long_password
will also be automatically treated as sensitive, even if sensitive = true
is not explicitly set in the
locals
block.
Note: Terraform requires that any root module output containing sensitive data be also explicitly marked as sensitive
with sensitive = true
.
Terraform Plan Output:
Let's assign a value to the short_password
input using the TF_VAR_short_password
environment
variable and run terraform plan
:
$ export TF_VAR_short_password=12345
$ terraform plan
Changes to Outputs:
+ password = (sensitive value)
...
Terraform Apply Output:
$ terraform apply -auto-approve
Changes to Outputs:
+ password = (sensitive value)
...
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
password = <sensitive>
Terraform Output Commands:
$ terraform output
password = <sensitive>
$ terraform output password
"secret12345"
$ terraform output -json
{
"password": {
"sensitive": true,
"type": "string",
"value": "secret12345"
}
}
Terraform Console:
$ terraform console
> var.short_password
(sensitive value)
> local.long_password
(sensitive value)
>
Terraform State File:
$ terraform show
Outputs:
password = (sensitive value)
$ cat terraform.tfstate
{
"version": 4,
"terraform_version": "1.11.2",
"serial": 12,
"lineage": "ae3e44bf-85dc-e7f5-ddb0-ba513fb1c676",
"outputs": {
"password": {
"value": "secret12345",
"type": "string",
"sensitive": true
}
},
"resources": [],
"check_results": null
}
As demonstrated by this example, Terraform masks the variables and outputs marked as sensitive
(with sensitive = true
) in CLI outputs, however, the state file still contains
such values in plaintext.
In the next section we will review Terraform mechanisms that may help addressing this issue.
Back to Top
The ephemeral
Attribute
Terraform's ephemeral
attribute is a new feature introduced in Terraform v1.10 that allows you
to mark certain values as temporary or non-persistent. When applied, it tells Terraform not to store the
marked value in the state file, making it useful for sensitive or short-lived data that should only exist
during runtime.
This is an important step forward for improving Terraform's handling of temporary secrets, one-time tokens,
or credentials that should never be retained once apply completes.
What Does ephemeral
Do?
When a value is marked with ephemeral = true
:
- It will not be written to the state file
- It will still be available during the plan and apply operations for internal computation
- It behaves similarly to
sensitive = true
, but with the added benefit of avoiding
storage in a state file.
Where Can ephemeral = true
Be Used?
variable
block
locals
block
output
block
Ephemeral Variables
You can mark an input variable as ephemeral by setting the ephemeral
argument to true
.
Ephemeral variables can be referenced only in specific contexts. The following are valid contexts for
ephemeral variables:
- write-only arguments
- local values
- ephemeral resources
- ephemeral outputs
- provider blocks
- provisioner and connection blocks
Note: Any expression that references an ephemeral variable will also be treated as ephemeral.
Ephemeral local
Values
A local value is implicitly treated as ephemeral if it is assigned a value that originates from
an ephemeral source.
Ephemeral Outputs
To mark an output in a child module as ephemeral, set the ephemeral
attribute to true
.
This output will not be written to the state file.
Ephemeral outputs can be referenced only in specific contexts. The following are valid contexts for
ephemeral outputs:
- write-only arguments
- local values
- ephemeral resources
Note that you cannot set an output
value as ephemeral
in the root module.
When to Use ephemeral
Attribute
Use ephemeral = true
when:
- You have temporary secrets or access tokens that are unsafe to store
- You are working with short-lived session credentials
- You need to ensure that nothing sensitive persists in the state
Avoid ephemeral = true
if:
- Your resource or module depends on state persistence
Back to Top
Ephemeral Blocks and Resources
An ephemeral block is a special construct, similar to a resource block, that describes one or more temporary ephemeral
resources only available during the Terraform run phase. Terraform does not store ephemeral resources in its state.
Structure of an Ephemeral Block:
ephemeral "<resource_type>" "<resource_name>" {
<attributes>
<meta-arguments>
}
The arguments within the body of an ephemeral
block vary by resource type and provider. An ephemeral
resource type's documentation usually lists available arguments and provides details on how to use them.
Supported Providers and Resources
Terraform's ephemeral
block is supported by only a limited set of providers and resource types,
some of which are listed below:
- HashiCorp
- GCP
google_service_account_access_token
google_service_account_id_token
google_service_account_jwt
google_service_account_key
- AWS
aws_kms_secrets
aws_secretsmanager_random_password
aws_secretsmanager_secret_version
- Azure
azurerm_key_vault_certificate
azurerm_key_vault_secret
While ephemeral
improves handling of sensitive values, it does not replace external secret managers
(like HashiCorp Vault, AWS Secrets Manager, or GCP Secret Manager). Instead, it complements secure workflows
by ensuring secrets used transiently are not inadvertently persisted.
Code Example with ephemeral
Block:
# main.tf
ephemeral "random_password" "pass_ephemeral" {
length = 12
}
resource "random_password" "pass_plain" {
length = 12
}
This Terraform code defines two ways to generate a random password - with an ephemeral block and a "regular" resource block.
Let's run terraform apply
and examine the resulting state file.
Terraform Apply Output:
$ terraform apply -auto-approve
ephemeral.random_password.pass_ephemeral: Opening...
ephemeral.random_password.pass_ephemeral: Opening complete after 0s
ephemeral.random_password.pass_ephemeral: Closing...
ephemeral.random_password.pass_ephemeral: Closing complete after 0s
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# random_password.pass_plain will be created
+ resource "random_password" "pass_plain" {
+ bcrypt_hash = (sensitive value)
+ id = (known after apply)
...
+ result = (sensitive value)
+ special = true
+ upper = true
}
Plan: 1 to add, 0 to change, 0 to destroy.
ephemeral.random_password.pass_ephemeral: Opening...
random_password.pass_plain: Creating...
ephemeral.random_password.pass_ephemeral: Opening complete after 0s
ephemeral.random_password.pass_ephemeral: Closing...
ephemeral.random_password.pass_ephemeral: Closing complete after 0s
random_password.pass_plain: Creation complete after 0s [id=none]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Terraform State File:
$ terraform show
# random_password.pass_plain:
resource "random_password" "pass_plain" {
bcrypt_hash = (sensitive value)
id = "none"
...
result = (sensitive value)
special = true
upper = true
}
$ cat terraform.tfstate
{
"version": 4,
"terraform_version": "1.11.2",
"serial": 49,
"lineage": "af6b68eb-ccfb-4143-8948-3ebbc0e04bf8",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "random_password",
"name": "pass_plain",
"provider": "provider[\"registry.terraform.io/hashicorp/random\"]",
"instances": [
{
"schema_version": 3,
"attributes": {
"bcrypt_hash": "$2a$10$Le.Z0haIlKqtyEPmp/mWSO7Pqixe8WHXuQTm3.U5IWEoyf.vuqEM2",
"id": "none",
"keepers": null,
"length": 12,
...
"override_special": null,
"result": "hfm84?kSl@QZ",
"special": true,
"upper": true
},
"sensitive_attributes": [
[
{
"type": "get_attr",
"value": "bcrypt_hash"
}
],
[
{
"type": "get_attr",
"value": "result"
}
]
]
}
]
}
],
"check_results": null
}
As we can see, while the random_password.pass_plain.result
value is included in the state file in plaintext,
the details of the ephemeral resource (ephemeral.random_password.pass_ephemeral
) are not present
in the Terraform output and excluded form the state file.
Next, we will explore how to use ephemeral
together with Terraform's write-only arguments.
Back to Top
Write-only Arguments in Terraform
Write-only arguments in Terraform allow you to configure sensitive values, such as passwords, private keys,
or credentials, that Terraform sends to the provider during resource creation or update, but does not store in the
state file and cannot read back once applied.
Write-only arguments
- accept both ephemeral and non-ephemeral values
- only available during the current Terraform operation
- cannot be read back by Terraform after creation
- do not appear in the state file or output
- identified by a "_wo" (write-only) suffix
- have an accompanying "version" arguments to track changes
Note: The functions of write-only arguments and their version arguments are provider- and resource-specific,
so consult the relevant documentation for more details.
Write-only arguments complement Terraform's ephemeral mechanisms, working together with the ephemeral attributes
or blocks to prevent accidental exposure of secrets in plaintext in local or remote state.
Provider Dependency and Limited Support
Write-only arguments are not a core Terraform language feature, but instead implemented at the provider level.
Whether a particular resource supports them depends entirely on the provider's resource schema.
Here are some examples of resources supporting write-only arguments:
Provider |
Resource |
Write-only Argument |
GCP |
google_secret_manager_secret_version |
secret_data_wo |
GCP |
google_sql_user |
password_wo |
GCP |
google_bigquery_data_transfer_config |
secret_access_key_wo |
AWS |
aws_secretsmanager_secret_version |
secret_string_wo |
AWS |
aws_db_instance |
password_wo |
Azure |
azurerm_key_vault_secret |
value_wo |
As shown in the table, write-only arguments are distinctly identified with a "_wo" suffix.
Code Example with Write-only Argument:
# main.tf
variable "project" {
type = string
description = "GCP project to create secret in"
}
ephemeral "random_password" "api_token" {
length = 24
lower = false
special = false
}
resource "google_secret_manager_secret" "secret_token" {
secret_id = "api-token"
project = var.project
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "secret_token_version" {
secret = google_secret_manager_secret.secret_token.id
secret_data_wo = ephemeral.random_password.api_token.result
secret_data_wo_version = 1
}
output "token" {
value = {
token = google_secret_manager_secret.secret_token.name
value = google_secret_manager_secret_version.secret_token_version.secret_data
}
sensitive = true
}
How It Works:
This configuration generates a random password and stores it securely in Google Cloud Secret Manager
without ever exposing the password in the Terraform state file or command-line output.
Here are more details about the resource blocks included in the configuration:
ephemeral "random_password" "api_token"
- This block uses the
ephemeral
resource type which
behaves like a regular resource, but its data is not saved in the Terraform state file.
- It uses the
random_password
resource from the random
provider to generate
a 24-character password.
- The
lower = false
and special = false
attributes restrict the character types
used for password generation.
- The generated password (
.result
) is available for other resources during the
apply phase but is discarded immediately after, preventing it from being stored in plaintext in the
state file.
resource "google_secret_manager_secret" "secret_token"
- This defines a secret object in Google Secret Manager.
- It's named
api-token
within the GCP project specified by var.project
.
- The
replication { auto {} }
nested block tells GCP to automatically replicate
the secret across regions for high availability.
resource "google_secret_manager_secret_version" "secret_token_version"
- This resource creates a new version of the secret within the
"secret_token"
object created above.
secret_data_wo
: This is a write-only attribute. Terraform populates this with the result from the
ephemeral.random_password.api_token
resource.
It writes the value to Google Secret Manager but does not read it back into the state.
secret_data_wo_version
: This attribute is used to trigger updates. If we change this number
(e.g., from 1 to 2) and run terraform apply
, Terraform will know the write-only value has changed and
will create a new secret version with a new random password.
output "token"
- This output is marked as
sensitive = true
, so Terraform will hide its value in the console output.
- It attempts to output the secret's name and its value (
secret_data
). However, because we used
secret_data_wo
to create the secret version, the secret_data attribute
will be null
when read back by Terraform. This is the intended, secure behavior. The output will not actually contain the secret value.
Terraform Plan Output:
$ terraform plan
ephemeral.random_password.api_token: Opening...
ephemeral.random_password.api_token: Opening complete after 1s
ephemeral.random_password.api_token: Closing...
ephemeral.random_password.api_token: Closing complete after 0s
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# google_secret_manager_secret.secret_token will be created
+ resource "google_secret_manager_secret" "secret_token" {
...
}
# google_secret_manager_secret_version.secret_token_version will be created
+ resource "google_secret_manager_secret_version" "secret_token_version" {
+ create_time = (known after apply)
+ deletion_policy = "DELETE"
+ destroy_time = (known after apply)
+ enabled = true
+ id = (known after apply)
+ is_secret_data_base64 = false
+ name = (known after apply)
+ secret = (known after apply)
+ secret_data_wo = (write-only attribute)
+ secret_data_wo_version = 1
+ version = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ secret_version_id = (sensitive value)
Terraform Apply Output:
$ terraform apply -auto-approve
ephemeral.random_password.api_token: Opening...
ephemeral.random_password.api_token: Opening complete after 0s
ephemeral.random_password.api_token: Closing...
ephemeral.random_password.api_token: Closing complete after 0s
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# google_secret_manager_secret.secret_token will be created
+ resource "google_secret_manager_secret" "secret_token" {
...
}
# google_secret_manager_secret_version.secret_token_version will be created
+ resource "google_secret_manager_secret_version" "secret_token_version" {
+ create_time = (known after apply)
+ deletion_policy = "DELETE"
+ destroy_time = (known after apply)
+ enabled = true
+ id = (known after apply)
+ is_secret_data_base64 = false
+ name = (known after apply)
+ secret = (known after apply)
+ secret_data_wo = (write-only attribute)
+ secret_data_wo_version = 1
+ version = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ token = (sensitive value)
ephemeral.random_password.api_token: Opening...
ephemeral.random_password.api_token: Opening complete after 0s
google_secret_manager_secret.secret_token: Creating...
google_secret_manager_secret.secret_token: Creation complete after 1s [id=projects/psychic-trainer-320114/secrets/api-token]
google_secret_manager_secret_version.secret_token_version: Creating...
google_secret_manager_secret_version.secret_token_version: Creation complete after 0s [id=projects/463183993749/secrets/api-token/versions/1]
ephemeral.random_password.api_token: Closing...
ephemeral.random_password.api_token: Closing complete after 0s
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
token = <sensitive>
Terraform Output Command:
$ terraform output
token = <sensitive>
$ terraform output -json
{
"token": {
"sensitive": true,
"type": [
"object",
{
"token": "string",
"value": "string"
}
],
"value": {
"token": "projects/463183993749/secrets/api-token",
"value": null
}
}
}
Terraform Console:
$ terraform console
> google_secret_manager_secret_version.secret_token_version.secret_data
(sensitive value)
> google_secret_manager_secret_version.secret_token_version.secret_data_wo
tostring(null)
>
Terraform State File:
$ terraform show
# google_secret_manager_secret.secret_token:
resource "google_secret_manager_secret" "secret_token" {
...
}
# google_secret_manager_secret_version.secret_token_version:
resource "google_secret_manager_secret_version" "secret_token_version" {
create_time = "2025-08-04T19:33:00.097565Z"
deletion_policy = "DELETE"
destroy_time = null
enabled = true
id = "projects/463183993749/secrets/api-token/versions/1"
is_secret_data_base64 = false
name = "projects/463183993749/secrets/api-token/versions/1"
secret = "projects/psychic-trainer-320114/secrets/api-token"
secret_data_wo = (write-only attribute)
secret_data_wo_version = 1
version = "1"
}
Outputs:
token = (sensitive value)
$ cat terraform.tfstate
{
"version": 4,
"terraform_version": "1.11.2",
"serial": 28,
"lineage": "ed28a4bb-abf3-cf50-bacf-2a85b97c1b85",
"outputs": {
"token": {
"value": {
"token": "projects/463183993749/secrets/api-token",
value": null
},
"type": [
"object",
{
"token": "string",
"value": "string"
}
],
"sensitive": true
}
},
"resources": [
{
"mode": "managed",
"type": "google_secret_manager_secret",
"name": "secret_token",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"instances": [
...
]
},
{
"mode": "managed",
"type": "google_secret_manager_secret_version",
"name": "secret_token_version",
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"create_time": "2025-08-04T19:33:00.097565Z",
"deletion_policy": "DELETE",
"destroy_time": "",
"enabled": true,
"id": "projects/463183993749/secrets/api-token/versions/1",
"is_secret_data_base64": false,
"name": "projects/463183993749/secrets/api-token/versions/1",
"secret": "projects/psychic-trainer-320114/secrets/api-token",
"secret_data": null,
"secret_data_wo": null,
"secret_data_wo_version": 1,
"timeouts": null,
"version": "1"
},
"sensitive_attributes": [
[
{
"type": "get_attr",
"value": "secret_data"
}
]
],
"private": "eyJlMmJmYjcz...MDAwMDAwMDAwMH19",
"dependencies": [
"ephemeral.random_password.api_token",
"google_secret_manager_secret.secret_token"
]
}
]
}
],
"check_results": null
}
To update the write-only argument secret_data_wo
, increment the version argument's value in the configuration
(secret_data_wo_version = 2
) and re-run terraform apply
.
As we can see, the ephemeral api_token
resource is not stored in the state file. In addition, the sensitive
values secret_data
and secret_data_wo
are masked in the Terraform output as well as in the state file.
This demonstrates how ephemeral
resources, write-only arguments, and sensitive
attribute help prevent accidental leaks of sensitive data and make it easier to comply with security best practices.
Back to Top
Recommendations and Best Practices
Handling sensitive data correctly in Terraform is essential for maintaining infrastructure security, ensuring
regulatory compliance, and preventing accidental exposure of credentials and secrets. This section outlines best
practices and provides guidance on when to use specific Terraform features for sensitive data handling.
Always Use sensitive = true
for Outputs Involving Secrets
Terraform's sensitive = true
attribute prevents values from being displayed in the terminal during plan
and apply. It does not encrypt or redact values in state files, but helps avoid exposure during CLI operations.
Use when:
- Outputting secrets (passwords, tokens, keys) from a resource or module
- Hiding values in command-line output
Caution:
- Values still appear in the Terraform state file unless you use write-only arguments or ephemeral features.
Use Write-only Arguments for Secrets That Should Not Be Stored in State
Write-only arguments are assigned only during apply, and are not stored in the state. This offers true protection against
persistent secret exposure.
Use when:
- Supplying passwords, private keys, or access tokens that should not be stored
- Resources support write-only ("_wo") arguments
Limitations:
- Only available for certain resources and providers.
- Cannot read or directly reference these values elsewhere.
Use Ephemeral Values to Avoid Persisting Sensitive Data
Ephemeral variables and outputs (via ephemeral = true
) ensure Terraform does not store or expose
output values at all. These values are discarded after apply.
Use when:
- Outputting values from modules that must be used during runtime but never persisted
- Providing downstream input to another module during the same apply only
Use Ephemeral Resource to Avoid Storing Data in State File
Ephemeral blocks and resources ensure Terraform does not store sensitive values in state.
Use when:
- Using sensitive value that must be used immediately but never persisted.
Limitations:
- Only available for certain resources and providers.
Avoid Using Variables to Store Secrets
While Terraform allows setting variable values via TF_VAR_*
, .tfvars
, or CLI input,
these are not secure by default. Prefer environment-based secret injection or integrations with CI/CD secret
stores.
If unavoidable:
- Use
sensitive = true
in variable definitions.
- Avoid sensitive values in outputs.
To conclude:
- Use ephemeral and write-only features in combination when supported to minimize the footprint of secrets
in infrastructure as code.
- Protect your state files - store them in encrypted and access-controlled backends.
- Regularly audit your Terraform state, outputs, and logs.
- Periodically review Terraform provider documentation to stay aware of security features, as well as new
supported ephemeral resources and write-only arguments
See Also:
Understanding Terraform Variable Precedence
Terraform Value Types Tutorial
Terraform count
Explained with Practical Examples
Terraform for_each
Tutorial with Practical Examples
Exploring Terraform dynamic
Blocks with GCP Examples
Working with External Data in Terraform
Terraform Modules FAQ