How to Fix Terraform 'known after apply' for Sensitive Output Values (State Leak Prevention)
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: An
outputblock references a resource attribute that is computed at apply-time (e.g., a generated secret, RDS password, or KMS key ARN), and Terraform cannot resolve its value duringplan. Ifsensitive = trueis absent, the value may be printed in plaintext to stdout and persisted unredacted interraform.tfstate. - How to fix it: Add
sensitive = trueto every output referencing a secret. Forknown after applyblocking downstream modules, restructure withdepends_onor useterraform_data/null_resourceto enforce ordering. - Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your
outputs.tfand get back a hardened, diff-annotated fix without sending secrets to any server.
▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]
The Incident (What Does the Error Mean?)
During terraform plan, you see output similar to this:
Changes to Outputs:
+ db_master_password = (sensitive value)
# (known after apply)
+ api_secret_key = (known after apply)Or worse — no (sensitive value) label at all:
Changes to Outputs:
+ rds_password = "will-be-set-after-apply"
# (known after apply)Immediate consequence: Terraform is telling you it cannot resolve this value until the resource is created. The danger is two-fold:
- Missing
sensitive = true— the value, once resolved at apply-time, is printed in cleartext to your terminal and CI/CD logs. - State file exposure —
terraform.tfstatestores ALL output values in plaintext JSON regardless of thesensitiveflag unless you're using encrypted remote state (S3+KMS, Terraform Cloud, etc.). Aknown after applyvalue that resolves to a 32-character database password will sit in that state file forever.
▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]
The Attack Vector / Blast Radius
This is not a theoretical risk. Here is the concrete exploit chain:
Vector 1 — CI/CD Log Scraping:
Your GitHub Actions or GitLab CI runner executes terraform apply. Without sensitive = true, the resolved password prints to stdout. CI logs are often retained for 90+ days, accessible to every developer with repo read access, and sometimes inadvertently exposed via misconfigured log aggregators (Datadog, Splunk, CloudWatch Logs). An attacker with read access to your CI system harvests the credential with zero infrastructure access required.
Vector 2 — State File Exfiltration:
If your S3 backend bucket has a misconfigured bucket policy (overly permissive IAM, public ACL left from a misconfiguration), an attacker who gains read access to terraform.tfstate gets every output value in plaintext — passwords, tokens, private keys, connection strings. The sensitive = true flag does not encrypt the state file. It only suppresses terminal output.
Vector 3 — Module Output Propagation:
A child module marks an output as sensitive = true. A parent module consumes it and re-outputs it without sensitive = true. Terraform strips the sensitivity marker at the boundary. The secret is now a non-sensitive output in the parent — logged, printed, and stored without redaction.
Blast radius: Full credential compromise for any service whose secret is exposed. In a typical setup this means RDS master password, API keys, TLS private keys, or OAuth client secrets — the keys to your entire data plane.
How to Fix It (The Solution)
Basic Fix — Add `sensitive = true` to Output Blocks
This is the minimum viable fix. It suppresses terminal output and forces consumers to explicitly acknowledge they are handling a sensitive value.
output "db_master_password" {
- value = aws_db_instance.main.password
+ value = aws_db_instance.main.password
+ sensitive = true
}
output "api_secret_key" {
- value = aws_secretsmanager_secret_version.api_key.secret_string
+ value = aws_secretsmanager_secret_version.api_key.secret_string
+ sensitive = true
}Why known after apply still appears even after this fix: The sensitive = true flag does not resolve the computed attribute problem. If the resource hasn't been created yet, the value is genuinely unknown at plan time. This is expected and correct behavior — (sensitive value) (known after apply) is the safe state. The dangerous state is the value being known and printed without the sensitive label.
Enterprise Best Practice — Defense-in-Depth Secret Handling
Do not output secrets as Terraform outputs at all if you can avoid it. Use AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault as the authoritative secret store, and reference secrets by ARN/path — never by value.
Pattern: Output the secret ARN, not the secret value
# BAD: Outputting the raw credential
output "rds_password" {
- value = aws_db_instance.main.password
- sensitive = true
}
# GOOD: Output only the reference — consuming services fetch the value at runtime
+output "rds_secret_arn" {
+ value = aws_secretsmanager_secret.rds_master.arn
+ description = "ARN of the RDS master password secret. Fetch value at runtime via AWS SDK."
+}Pattern: Prevent sensitivity stripping across module boundaries
# child module: modules/database/outputs.tf
output "connection_string" {
value = "postgresql://${var.user}:${random_password.db.result}@${aws_db_instance.main.endpoint}/app"
+ sensitive = true
}
# root module: outputs.tf — MUST re-declare sensitive = true
output "database_connection_string" {
value = module.database.connection_string
+ sensitive = true # Without this, sensitivity is stripped at module boundary
}Pattern: Encrypted remote state is non-negotiable
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
+ encrypt = true
+ kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"
dynamodb_table = "terraform-state-lock"
}
}Pattern: Use precondition to enforce sensitivity contracts in Terraform 1.2+
output "db_master_password" {
value = aws_db_instance.main.password
sensitive = true
+
+ precondition {
+ condition = length(aws_db_instance.main.password) >= 16
+ error_message = "RDS master password must be at least 16 characters. Check random_password resource."
+ }
}💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.
(For CI/CD pipeline automation, install via
npm i -g stackengine).
▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]
Prevention in CI/CD
Fix it once in code. Enforce it forever in the pipeline.
1. Checkov — Catch Missing `sensitive = true` Pre-Commit
# .github/workflows/terraform-security.yml
- name: Run Checkov on Terraform outputs
uses: bridgecrewio/checkov-action@master
with:
directory: .
check: CKV_TF_1 # Ensure sensitive outputs are marked
framework: terraform
soft_fail: falseCheckov rule CKV_TF_1 flags any output block that references a password, secret, key, or token attribute without sensitive = true.
2. OPA/Conftest Policy — Block Plaintext Secret Outputs in Plan
# policies/no_plaintext_outputs.rego
package terraform.outputs
deny[msg] {
output := input.planned_values.outputs[name]
output.sensitive == false
regex.match(`(?i)(password|secret|key|token|credential)`, name)
msg := sprintf("Output '%v' appears to contain a secret but is not marked sensitive = true", [name])
}Run in CI:
terraform show -json tfplan.binary | conftest test --policy policies/ -3. `tfsec` Static Analysis
tfsec . --include-passed --format sarif --out tfsec-results.sariftfsec rule GEN006 specifically targets sensitive outputs without the sensitive flag.
4. Pre-Commit Hook — Block Accidental State File Commits
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.0
hooks:
- id: terraform_tfsec
- id: terraform_checkov
- id: terraform_validate
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: detect-private-key
- id: check-added-large-files # Catches accidental tfstate commits5. Enforce State Encryption via Sentinel (Terraform Enterprise/Cloud)
# sentinel/enforce-encrypted-state.sentinel
import "tfconfig/v2" as tfconfig
all_backends_encrypted = rule {
all tfconfig.terraform as _, tf {
all tf.backend as backend_type, config {
backend_type is "s3" implies config.values.encrypt is true
}
}
}
main = rule { all_backends_encrypted }This policy blocks any terraform apply in TFC/TFE if the S3 backend does not have encrypt = true configured.
Official Grounding & Normative Specifications
The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:
Core concepts governing declarative infrastructure lifecycle, state lock contention, and provider graph synthesis.
Federal security guidelines for secret exfiltration prevention, automated integrity checks, and peer sign-offs.
Core memory subsystem controls, pressure stall information (PSI), and kernel OOM invocation mechanics.
Industry-standard specification for container execution boundaries and POSIX namespace isolation.