WintelGuy.com

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
    • random_password
  • 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