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:
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:
output "rds_password" {
value = random_password.db.result
}You'd write:
output "rds_password" {
value = random_password.db.result
sensitive = true
}When you run terraform apply or terraform output, you'll see:
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.
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:
export TF_VAR_api_key="my_secret_key"
terraform applyTerraform 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:
terraform showYou'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
*.tfstateand*.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:
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:
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:
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:
- name: Run Terraform
env:
TF_VAR_api_key: ${{ secrets.API_KEY }}
run: |
terraform init
terraform apply -auto-approveThe 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 = trueto 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
*.tfstatefiles 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.






Comments
Loading comments…