Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 085 / 100

Sensitive values in Terraform output in Terraform & IaC

Hide secrets in logs.

Expona Admin6 min read

Introduction

Secrets leak. Database passwords, API keys, and cloud credentials end up in logs, Git history, and CI/CD output more often than we'd like to admit. When you work with Terraform, the problem compounds because infrastructure code often contains references to sensitive values, and by default Terraform will happily print them to stdout and store them in state files. In this article, we'll walk through how to handle sensitive values in Terraform output in Terraform & IaC, protecting your secrets at multiple layers—from variable declaration through CI/CD execution.

Understanding the sensitivity problem

Let's start with why this matters. You run a Terraform apply, and the console outputs something like:

hcl
Outputs:

database_password = "MyS3cr3tP@ssw0rd"
api_key = "sk_live_51Abc123Def456"

That output scrolls through your CI/CD logs, gets captured in build artifacts, and might be visible to anyone with access to your pipeline. Even if you're careful, a colleague forwarding a screenshot of a successful deploy could expose credentials. This is where marking outputs and variables as sensitive becomes critical.

Marking outputs as sensitive

The simplest protective measure is adding sensitive = true to your output blocks. When you do this, Terraform will:

  • Redact the value from console output, displaying <sensitive> instead
  • Still store the actual value in the state file (more on that below)
  • Prevent the value from being interpolated into logs in most scenarios

Here's a practical example. Instead of:

hcl
output "rds_password" {
  value = random_password.db.result
}

You'd write:

hcl
output "rds_password" {
  value     = random_password.db.result
  sensitive = true
}

When you run terraform apply or terraform output, you'll see:

hcl
Outputs:

rds_password = <sensitive>

The value is still there in state; it's just hidden from the human-readable output. This is a baseline measure that keeps secrets out of CI logs and developer terminals.

Marking variables as sensitive

You should also mark input variables as sensitive when they'll receive secret values. This prevents Terraform from echoing them back during planning or apply operations.

hcl
variable "api_key" {
  type      = string
  sensitive = true
  description = "External API key for third-party service"
}

If someone passes this via a command flag or environment variable, Terraform won't print it to the console. This is especially important for automation—your CI/CD system can safely pass secrets as environment variables or through secret management systems without them appearing in build logs.

In practice, you might inject it like:

hcl
export TF_VAR_api_key="my_secret_key"
terraform apply

Terraform will accept the variable but won't display it in the plan or apply output.

The critical limitation: State files still contain plain text

Here's the hard truth that catches many teams: marking a value as sensitive = true does not encrypt it in the state file. If you run:

hcl
terraform show

You'll see the raw secret in state. The sensitivity flag only hides it from console output and certain Terraform operations. The actual plaintext lives in your local terraform.tfstate or remote state bucket.

This means you must:

  • Never commit state files to Git. Add *.tfstate and *.tfstate.* to .gitignore.
  • Enable encryption at rest for remote state. Use AWS S3 with encryption, Azure Blob Storage with encryption, or Terraform Cloud's encrypted backend.
  • Restrict access to state files. Use IAM policies, storage account permissions, or RBAC to limit who can read state.
  • Enable state file versioning and logging. Track who accessed state and when.

A common setup in production is storing state in an S3 bucket with these properties:

hcl
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state-prod"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket                  = aws_s3_bucket.terraform_state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

This ensures state is encrypted, versioned, and not publicly accessible—critical when it contains plaintext secrets.

When to use nonsensitive()

Terraform provides a nonsensitive() function to strip the sensitive flag from a value, but use it sparingly. There are legitimate cases:

  • Passing to non-sensitive downstream systems. If a managed service API accepts a token and you need to pass it through as a resource argument that doesn't support sensitivity, you might use it.
  • Logging in controlled environments. If you're working in a completely isolated, air-gapped environment with no external access, you might log a decrypted value for debugging.
  • Non-sensitive derived values. If you extract part of a secret for use in a non-sensitive context (e.g., a username from a connection string), nonsensitive might be appropriate.

But in most cases, nonsensitive() defeats the purpose. If you find yourself using it frequently, reconsider your architecture. Here's an example of where it's tempting but risky:

hcl
output "db_connection_string" {
  # Don't do this casually
  value     = "user=admin password=${nonsensitive(random_password.db.result)} host=db.example.com"
  sensitive = false
}

You've now exposed the password in plain sight again. Instead, keep the entire connection string marked as sensitive:

hcl
output "db_connection_string" {
  value = "user=admin password=${random_password.db.result} host=db.example.com"
  sensitive = true
}

Masking values in CI/CD logs

Even with sensitivity flags, your CI/CD system should add another layer: secret masking. Most platforms—GitHub Actions, GitLab CI, Jenkins, CircleCI—support automatically redacting known secrets from logs.

In GitHub Actions, for example:

hcl
- name: Run Terraform
  env:
    TF_VAR_api_key: ${{ secrets.API_KEY }}
  run: |
    terraform init
    terraform apply -auto-approve

The value of secrets.API_KEY is automatically masked in logs, appearing as *** anywhere it's printed. This complements Terraform's sensitivity markers.

For additional protection, configure your CI/CD to:

  • Explicitly list which variables or outputs should never be logged
  • Disable verbose logging in production pipelines
  • Use short-lived credentials with restricted scope for each pipeline run
  • Archive logs with limited retention and access controls

Common mistakes

Mistake 1: Assuming sensitive = true encrypts state. It doesn't. Teams often feel safe marking a password as sensitive and then commit state to Git. Disaster. Treat state files as you would a plaintext password manager—never in version control, always encrypted at rest.

Mistake 2: Mixing sensitive and non-sensitive in composite outputs. If you build a complex output like a JSON configuration file containing both public and secret values, the entire output should be marked sensitive. Otherwise, the non-sensitive parts might still leak context about the secrets.

Mistake 3: Forgetting to mask in local development. Developers running Terraform locally also need the discipline not to share state files, screenshots of apply output, or logged values. Communicate that sensitive data handling isn't just a production concern.

Mistake 4: Over-relying on CI/CD masking. If your pipeline masks a secret from logs but stores it unencrypted in a shared artifact, you've gained nothing. The protection must be layered: sensitivity flags in Terraform, encryption in state, masking in CI, and access control throughout.

Key takeaways

  • Mark outputs and variables as sensitive = true to hide them from console output and CI logs.
  • Understand that sensitivity hides values from display but does not encrypt them in state files.
  • Always encrypt remote state at rest, version it, and restrict access via IAM or RBAC.
  • Never commit *.tfstate files to Git; use .gitignore.
  • Use nonsensitive() rarely and only when you've carefully considered the implications.
  • Layer security: Terraform sensitivity + state encryption + CI/CD secret masking + access controls.
  • Treat state files with the same care as a plaintext secrets file—because they essentially are one.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles