Skip to content
apikey.tools
ENVEnvironment

Terraform & CloudFormation Secret Variable Builder

Declare secret variables correctly — and understand why sensitive = true does not encrypt state.

ENVRuns in this tabInput not transmitted

Input

Output

computing

Infrastructure as code has a specific and widely misunderstood secret problem: the state file.

sensitive = true does not encrypt anything

Marking a Terraform variable sensitive = true redacts it from plan and apply output. That is all it does.

The value is stored in plaintext in the state file. Every secret your configuration touches — variables, resource attributes, data source results — is written to terraform.tfstate in the clear.

The consequences follow directly:

  • Never commit terraform.tfstate. Add *.tfstate* to .gitignore.
  • Use a remote backend with encryption at rest: S3 with SSE-KMS, or HCP Terraform.
  • Restrict read access to the state backend as tightly as to the secrets manager itself. Anyone who can read state can read every secret.
  • Never post state output in a ticket or a chat channel.

Read secrets rather than pass them

The better pattern is to keep the value out of your configuration entirely by reading it at apply time:

hcl
data "aws_secretsmanager_secret_version" "api_key" {
  secret_id = "production/api_key"
}

locals {
  api_key = jsondecode(
    data.aws_secretsmanager_secret_version.api_key.secret_string
  )["API_KEY"]
}

The value never enters a .tfvars file, never sits in a CI environment variable, and never appears in your shell history. It still lands in state — nothing avoids that — but the number of places it exists drops sharply.

Better still: generate in-provider

The strongest pattern is to have the provider generate the secret so it never transits your machine at all. CloudFormation's GenerateSecretString does this: the stack creates the secret and passes only its ARN to consumers.

yaml
ManagedSecret:
  Type: AWS::SecretsManager::Secret
  Properties:
    GenerateSecretString:
      PasswordLength: 48
      ExcludePunctuation: true

Pass ARNs and references between resources, not values. A Lambda that receives a secret ARN and resolves it at runtime is strictly better than one that receives the secret as an environment variable.

CloudFormation NoEcho

NoEcho: true hides a parameter from the console, the API and describe-stacks. Like Terraform's sensitive, it is a display control. Parameter values are still recorded in CloudTrail and in the stack's history.

Validation catches the mistake early

The generated variable includes a length validation. A short or empty secret then fails at plan rather than at runtime, which turns a confusing production error into an obvious local one.

Frequently asked questions

Is the state file really plaintext?

Yes. Terraform stores all attribute values, including sensitive ones. sensitive affects console output only. This is documented behaviour and it surprises people every time.

Can I use a local state file if I encrypt the disk?

Better than nothing, but you lose locking, versioning and team access control. Use a remote backend with encryption at rest — it solves several problems at once.

How do I pass a secret into CI without it appearing in logs?

Use the CI system's secret store, which masks values in output, and prefer OIDC-based cloud authentication over static credentials entirely. GitHub Actions, GitLab CI and others can assume an AWS role without any long-lived key.

What about tfvars files?

Never commit one containing secrets. *.tfvars belongs in .gitignore — the gitignore generator includes it along with the state file patterns.

More environment tools

Emit secrets into the config format you deploy