Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 11 min read

Migrating Terraform Secrets to Write-Only Arguments

Sensitive doesn't keep secrets out of state — it only hides them in output. Here's how to migrate existing secret arguments to write-only variants so plaintext stops landing in state.

  • #terraform
  • #ai
  • #write-only
  • #secrets
  • #security

There’s a comfortable myth in a lot of Terraform codebases: “we marked the password sensitive, so it’s protected.” It isn’t. The sensitive flag redacts a value in CLI output and plans. It does nothing to stop that value from being written into your state file in plaintext, where it sits — readable by anyone with state access, preserved in remote-state version history, copied into every backup.

Write-only arguments fix this for real. The harder, more valuable job is retrofitting them onto a config that’s already leaking. Let’s do that properly.

Why sensitive isn’t enough

Consider a database password fed from a variable:

resource "aws_db_instance" "main" {
  username = "app"
  password = var.db_password   # lands in state, plaintext
}

Even if var.db_password is declared sensitive, the value is stored in the state file as part of the resource’s attributes. Pull the state and you can read it. That’s the whole problem.

What write-only arguments do differently

Newer provider versions expose write-only variants of secret arguments — for example password_wo instead of password — paired with a password_wo_version field:

resource "aws_db_instance" "main" {
  username            = "app"
  password_wo         = ephemeral.aws_secretsmanager_secret_version.db.secret_string
  password_wo_version = 1
}

The value passed to password_wo is sent to the provider but never stored in state. Because Terraform has nothing stored to diff against, it can’t detect when the underlying secret changes — so you control re-sends with password_wo_version. Bump the integer and Terraform sends the value again; leave it and re-runs are a no-op.

Step one: confirm support before anything else

Write-only support is provider- and version-specific. Not every resource has a _wo variant, and the exact spelling differs. Before writing a line of migration, confirm the variant exists for your exact resource and provider version. This is the single most important guardrail when you involve AI — a model will happily invent a password_wo that the provider never shipped, and you won’t find out until apply fails.

When you ask an assistant for help, demand it verify support first:

I want to migrate aws_db_instance.password to a write-only argument on the AWS provider v5.x. First confirm whether password_wo and password_wo_version actually exist for this resource at this version. If they don’t, tell me — don’t assume. Then show the migration.

A trustworthy answer either cites the supported variant or says plainly that it’s unavailable and proposes the next-best mitigation. Our write-only argument migration prompt builds that support check in as the first step.

Step two: source the value ephemerally

Write-only arguments must be fed from values that aren’t themselves persisted. A plain variable lands in state, defeating the purpose. Use an ephemeral resource or a secrets-manager data source:

ephemeral "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/db/password"
}

resource "aws_db_instance" "main" {
  username            = "app"
  password_wo         = ephemeral.aws_secretsmanager_secret_version.db.secret_string
  password_wo_version = 1
}

Ephemeral resources exist only during the run and are never written to state, so the secret flows from the secret store to the provider without ever being persisted by Terraform. Our ephemeral and write-only secrets prompt covers wiring these end to end.

Step three: understand the version trigger

Because Terraform stores nothing to compare, rotating the secret looks like a no-op until you change password_wo_version:

# After rotating the secret in Secrets Manager:
password_wo_version = 2   # forces Terraform to re-send the new value

This surprises people the first time. The version integer is the only lever that forces a re-send. It’s not tracking the secret’s content; it’s a manual “the value changed, push it again” signal. Document the convention so teammates bump it when they rotate.

Step four: the part you can’t skip — treat the old secret as burned

Switching to password_wo stops new leakage. It does nothing about the secret already sitting in your current state, your remote-state version history, and your backups. That value should be considered exposed, full stop.

So the migration isn’t done when the plaintext stops appearing in new plans. It’s done when:

  1. The new write-only config is applied and the secret is no longer in current state (terraform state show reveals nothing).
  2. The previously-stored secret has been rotated in the secret store, invalidating the value that leaked.

Skipping the rotation turns a real security fix into a feel-good config edit. If the value was ever in state, rotate it.

Step five: verify

After applying, confirm the win and the idempotency:

# Should show password_wo with no plaintext value
terraform state show aws_db_instance.main

# Re-running without bumping the version should be a clean no-op
terraform plan

If state show still reveals a secret, the migration is incomplete. If a plain re-run wants to change something, your version handling is off.

The migration checklist

  • Confirm the _wo variant exists for your exact provider and resource version — don’t assume, and don’t let AI assume either.
  • Feed it from an ephemeral resource or data source, never a plain variable.
  • Use _wo_version as the deliberate re-send trigger and document the convention.
  • Rotate the old secret — anything that was ever in state, history, or backups is compromised.
  • Verify with state show and a clean re-plan.

Write-only arguments are one of the few genuinely watertight ways to keep secrets out of Terraform state, but only if you treat the retrofit as a security operation rather than a syntax swap. Let AI draft the wiring and the version handling, then verify the state is clean and rotate the secret yourself. For more on keeping secrets out of your IaC, see the rest of our Terraform security guides.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.