Initializing Enclave...

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 output block 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 during plan. If sensitive = true is absent, the value may be printed in plaintext to stdout and persisted unredacted in terraform.tfstate.
  • How to fix it: Add sensitive = true to every output referencing a secret. For known after apply blocking downstream modules, restructure with depends_on or use terraform_data / null_resource to enforce ordering.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your outputs.tf and get back a hardened, diff-annotated fix without sending secrets to any server.

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:

  1. Missing sensitive = true — the value, once resolved at apply-time, is printed in cleartext to your terminal and CI/CD logs.
  2. State file exposureterraform.tfstate stores ALL output values in plaintext JSON regardless of the sensitive flag unless you're using encrypted remote state (S3+KMS, Terraform Cloud, etc.). A known after apply value that resolves to a 32-character database password will sit in that state file forever.

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 configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


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: false

Checkov 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.sarif

tfsec 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 commits

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

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →