Unmasking Sensitive Values in Terraform

Terraform upgrades or complex debugging sessions can be a real headache, especially when your configuration relies heavily on local variables. You run a terraform plan expecting to see the changes, but instead, you're greeted by a wall of (sensitive value)
. Computed local values are often mistakenly marked as sensitive, making it nearly impossible to review the plan output.
The typical workaround involves manually wrapping each value in a nonsensitive()
block, but that's tedious and pollutes your code. It would have been much easier if there were a command-line flag in Terraform to treat all sensitive attributes as non-sensitive. There has to be a better way, right? After many failed attempts, the solution became clear: instead of fighting the symptoms, you have to target the source by patching the Terraform binary itself.
TLDR; Patch IsBeforeSensitive()
and IsAfterSensitive()
methods in the Terraform binary to always return false
.
The Rabbit Hole of Failed Attempts
Before landing on the final solution, I went down a rabbit hole trying every trick I could think of to unmask these values. Here’s a brief list of the unsuccessful attempts:
Downloading the state file: The first logical step was to pull the state from the remote backend to manipulate it locally.
Editing the state file: I manually edited the downloaded
terraform.tfstate
file. I created a Vim macro to quickly empty the sensitive_attributes array, which was satisfying to watch.# search for `sensitive_attributes` /sensitive_attributes [ENTER] # macro qqnf[di[<ESC>q # repeat macro for 200 times 200@q
Patching the backend block: I then modified the Terraform configuration's backend block to use the local, edited state file instead of the remote one.
terraform { backend "local" { path = "terraform.tfstate" } }
Clearing the local cache: I removed the
.terraform/terraform.tfstate
file, which caches remote backend information, to ensure Terraform used my local backend.rm .terraform/terraform.tfstate
Patching core functions: My initial attempts to patch Terraform's
Sensitive()
andIsSensitive()
functions to behave like NonSensitive() also failed to produce the desired result.func Nonsensitive(v cty.Value) (cty.Value, error) { return NonsensitiveFunc.Call([]cty.Value{v}) } // patched functions func Sensitive(v cty.Value) (cty.Value, error) { return NonsensitiveFunc.Call([]cty.Value{v}) } func Issensitive(v cty.Value) (cty.Value, error) { return NonsensitiveFunc.Call([]cty.Value{v}) }
Investigating the provider: Since patching the core functions didn't work, I suspected the provider might be the cause. I dove into the terraform-provider-aws source code to see if it was responsible for the sensitivity logic.
Despite all this, every terraform plan still resulted in the same masked, sensitive output.
The Real Culprit: Provider vs. Core Binary Logic
The investigation into the provider source code was the final clue. It revealed a crucial distinction in how Terraform operates: a client-server architecture between the Terraform binary and its providers.
Think of it this way:
Provider (Server): A provider like terraform-provider-aws is responsible for managing the actual resources. Its job is to define the resource schema and simply declare which attributes should be considered sensitive (e.g.
password
on anaws_db_instance
resource). It essentially puts asensitive
label on the data it handles.Terraform Binary (Client): Terraform binary consumes this information. When it runs a plan, it's the binary's job to handle that "sensitive" label. It traces the dependencies, and if a value is derived from anything the provider labeled as sensitive, the binary taints the result as sensitive, too. Most importantly, the binary is solely responsible for rendering the final plan output.
This is why none of the earlier attempts worked. Although a provider marks a value as sensitive, it's the Terraform binary that ultimately decides what to show on your screen. It compares the before value (what's currently in the state) with the after value (what's calculated for the plan) and determines whether to mask the output as (sensitive value). This final rendering decision happens entirely within the Terraform binary's logic, not the provider's.
Solution: Patching IsBeforeSensitive
and IsAfterSensitive
Methods
The only effective way to stop this behavior is to intercept the functions within the Terraform binary responsible for checking sensitivity in the plan output. Within Terraform's source code, two methods are the gatekeepers for this:
IsBeforeSensitive()
: This function checks if a value was considered sensitive before the planned changes.IsAfterSensitive()
: This function checks if a value will be considered sensitive after the planned changes are applied.// treat sensitive attributes in the state as non-sensitive func (change Change) IsBeforeSensitive() bool { return false } // treat sensitive attributes during the plan as non-sensitive func (change Change) IsAfterSensitive() bool { return false }
By modifying the Terraform binary to make these two functions always return false
, you effectively disable the sensitivity check for the plan's output. Every value, regardless of its origin, will be displayed in plain text, making your debugging and upgrade reviews significantly easier. You can finally see what's actually changing.
A Word of Caution
This is a "use-at-your-own-risk" technique. Modifying a software binary should only be done for temporary or local debugging purposes in a controlled environment. Never use a patched binary in your CI/CD pipelines or for production deployments, as it completely defeats a critical security feature of Terraform. When you're done debugging, revert to the use of the official Terraform binary.
Subscribe to my newsletter
Read articles from dimm3r directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
dimm3r
dimm3r
I like breaking things apart.