RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
Terraform Sensitive Output Value Auditor
02. Root Cause Vector

Terraform exposes a 'known after apply' sensitive output, leaking secret resolution timing and risking plaintext state storage of credent...

03. Rapid Prescription

Refresh remote state lock, ensure provider version constraints, and audit state drift.

Zero-Trust Client IDE/ Security

Terraform Sensitive Output Value Auditor

Terraform exposes a 'known after apply' sensitive output, leaking secret resolution timing and risking plaintext state storage of credentials.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(77L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# ========================================================
# legacy-infra/terraform/modules/pci-secure-zone/main.tf
# This module provisions resources in the PCI secure zone.
# WARNING: This is a legacy module with tech debt from the 2022 migration.
# ========================================================

# TODO: Refactor this module to use the new secrets manager (JIRA: INFRA-7789)
# Currently using a mix of local vars and data sources due to legacy constraints.

provider "aws" {
  region = "us-east-1"
  # Using the old IAM role from the legacy account
  assume_role {
    role_arn = "arn:aws:iam::123456789012:role/legacy-pci-admin"
  }
}

# This data source fetches the latest AMI from our internal registry
# Note: The registry sometimes returns inconsistent IDs (known issue).
data "aws_ami" "legacy_app_ami" {
  most_recent = true
  owners      = ["self"]

  filter {
    name   = "name"
    values = ["my-registry.internal/legacy-api-*" ]
  }
}

# Main EC2 instance for the legacy payment API
resource "aws_instance" "pci_legacy_api" {
  ami           = data.aws_ami.legacy_app_ami.id
  instance_type = "t3.medium"
  subnet_id     = var.subnet_id  # Passed from the root module

  # TODO: Move secrets to AWS Secrets Manager (blocked by compliance review)
  user_data = <<-EOT
    #!/bin/bash
    export DB_URI="postgresql://app_user:LegacyDbP@ssw0rd!2023@pci-db.internal:5432/payments"
    export STRIPE_API_KEY="sk_live_51KjHp2LmQ9XyZABCdEfGHiJkLmNopQrStUvWxYz"
    export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
    export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    # Start the legacy service
    /opt/legacy-api/bin/start.sh
  EOT

  tags = {
    Name        = "legacy-pci-api-${terraform.workspace}"
    Environment = terraform.workspace
    # Old tagging convention from the pre-2022 era
    BillingCode = "PCI-LEGACY-${var.billing_suffix}"
  }
}

# Output the instance's private IP for the load balancer configuration
output "instance_private_ip" {
  value = aws_instance.pci_legacy_api.private_ip
}

# VULNERABILITY: This output references a sensitive value that depends on a resource attribute only known after apply
# The user_data contains embedded secrets, and the hash is computed during apply, causing 'known after apply' for a sensitive output.
output "instance_user_data_hash" {
  value     = sha256(aws_instance.pci_legacy_api.user_data)
  sensitive = true
}

# Redundant output from an old refactor (commented out but kept for audit trail)
# output "instance_public_dns" {
#   value = aws_instance.pci_legacy_api.public_dns
# }

# TODO: Clean up this workaround after the networking team fixes the VPC peering (ETA: Q3)
# This local is used to hack around a cross-account routing issue
locals {
  hacky_route_fix = var.enable_hacky_route ? "10.0.${terraform.workspace == "prod" ? "1" : "2"}.0/24" : ""
}
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

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.

▼ 🔍 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:

  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 exposure — terraform.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.

▼ 🔍 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: 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.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Does adding `sensitive = true` to a Terraform output actually encrypt the value in the state file?
No. The `sensitive = true` flag is purely a display control that suppresses the value in terminal output and logs; it does not encrypt the value in the `terraform.tfstate` file. The root cause is that Terraform's state file stores all values, including sensitive ones, in plaintext by default. To protect secrets at rest, you must manually configure and manage an encrypted remote backend—such as S3 with `encrypt = true` and a KMS key, Terraform Cloud with encrypted state, or HashiCorp Vault as a backend. This fundamental design flaw leaves your secrets exposed if the state file is ever accessed, a critical oversight in native Terraform security.
Q2.Why does my output still show '(known after apply)' even after I added `sensitive = true`?
'Known after apply' and `sensitive = true` are independent mechanisms. The former indicates a computed resource attribute that doesn't exist until creation, which is expected. However, manually diagnosing this—especially when it appears on existing resources due to tainting, forced replacements, or misconfigured lifecycle rules—is highly error-prone and risky. A single misstep can inadvertently expose sensitive values in logs or trigger unintended resource replacements, expanding the security blast radius. Instead of risking manual fixes, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that resolves the output while enforcing strict sensitivity propagation.
Q3.How do I safely pass a sensitive Terraform output to a downstream module without stripping the sensitivity marker?
You must manually declare `sensitive = true` on every output across all module boundaries, as Terraform does not automatically propagate sensitivity markers. This manual process is a compliance nightmare at scale, especially in large mono-repos with complex module hierarchies, where a single missed declaration can strip sensitivity and leak secrets. For enterprise-scale, automated PR drafting under Maker-Checker review, upgrade to the StackEngine CI/CD Automator (Team Edition). It provides on-prem, automated CI/CD scanning and fixes across your entire codebase, ensuring consistent zero-trust security and eliminating manual errors in compliance-critical environments.