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

Encrypting Terraform State at the Source With OpenTofu State Encryption

Backend encryption protects state at rest, but OpenTofu encrypts state before it ever leaves your machine. Here's how client-side state encryption actually works.

  • #terraform
  • #opentofu
  • #state
  • #encryption
  • #security
  • #secrets

Here’s a thing most teams quietly accept: your Terraform state contains secrets in plaintext, and the only thing standing between those secrets and a curious engineer is backend permissions plus encryption-at-rest you’re trusting your cloud provider to handle. The S3 bucket is encrypted, sure — but anyone who can read the object gets plaintext state, and your cloud provider’s keys decrypt it transparently.

OpenTofu’s client-side state encryption is a genuinely different model. It encrypts state before it leaves your machine and only decrypts it after it’s pulled back. The backend — S3, GCS, whatever — never sees plaintext. This is one of the most compelling reasons to be running OpenTofu, and it’s worth understanding even if you’re still on Terraform.

Backend encryption vs client-side encryption

The distinction is the whole point, so let’s be precise:

  • Backend / at-rest encryption (what you have today): state is encrypted by the storage system after it arrives. The bytes traveling to the backend and sitting in transit decision points are plaintext from Terraform’s perspective. Whoever can read the object — or whoever holds the cloud KMS key — sees everything.
  • Client-side encryption (what OpenTofu adds): state is encrypted by OpenTofu itself before the HTTP request leaves. The backend stores ciphertext it cannot read. Even a fully compromised bucket leaks only ciphertext.

If your threat model includes “someone with bucket read access” or “a misconfigured IAM policy” — and it should — only client-side encryption actually closes that gap.

Configuring it

State encryption lives in a terraform {} block under encryption. You define a key provider (where the key comes from) and a method (how encryption is done), then bind them to state:

terraform {
  encryption {
    key_provider "aws_kms" "primary" {
      kms_key_id = "arn:aws:kms:us-east-1:111111111111:key/abcd-1234"
      region     = "us-east-1"
      key_spec   = "AES_256"
    }

    method "aes_gcm" "primary" {
      keys = key_provider.aws_kms.primary
    }

    state {
      method = method.aes_gcm.primary
    }

    plan {
      method = method.aes_gcm.primary
    }
  }
}

Note that plan is encrypted too — saved plan files also contain sensitive values, and a lot of pipelines pass tofu plan -out=plan.bin between CI stages where it could be intercepted. Encrypting plans closes that hole as well.

Key providers you can choose from

The key provider is pluggable, which matters for how you manage the key lifecycle:

  • pbkdf2 — derives a key from a passphrase. Simplest to start, but you’re responsible for distributing and rotating the passphrase. Fine for solo or small teams.
  • aws_kms / gcp_kms / azure — uses a cloud KMS key. The encryption key never leaves KMS in plaintext; OpenTofu gets a data key wrapped by KMS. This is the production answer for most teams already in a cloud.
  • openbao / Vault-style — for teams centralizing secrets in a Vault-compatible system.

A passphrase-based config looks like:

key_provider "pbkdf2" "passphrase" {
  passphrase = var.state_passphrase  # supply via TF_VAR, never commit
}

Migrating existing plaintext state without a self-lockout

This is the step that scares people, and the fallback mechanism is the safety net. You can’t just turn encryption on — your existing state is plaintext, and a fresh encryption config can’t read it. OpenTofu handles this with fallback:

terraform {
  encryption {
    key_provider "pbkdf2" "passphrase" {
      passphrase = var.state_passphrase
    }

    method "aes_gcm" "new" {
      keys = key_provider.pbkdf2.passphrase
    }

    state {
      method = method.aes_gcm.new

      # Read plaintext during migration, write encrypted.
      fallback {}
    }
  }
}

The empty fallback {} says “if you can’t decrypt the current state, treat it as unencrypted.” OpenTofu reads your plaintext state, and the next write encrypts it. Run one apply (or tofu plan + apply), confirm the state object is now ciphertext, then remove the fallback so plaintext is no longer accepted. That removal is what actually enforces encryption going forward.

The same fallback pattern handles key rotation: put the new key as the primary method and the old key in a fallback method, run once to re-encrypt, then drop the old key.

Operational gotchas

  • Lose the key, lose the state. This is the one that keeps people up at night, and it’s real. With KMS, protect the key from deletion and enable key rotation at the KMS level. With a passphrase, store it in a secrets manager with the same rigor you’d give a root credential. There is no recovery if the key is gone.
  • Everyone and every pipeline needs decrypt access. CI runners, every engineer, every automation — all need the key to read state. Plan the access distribution before you flip it on, or you’ll lock out your own pipeline.
  • terraform_remote_state consumers need the key too. If another configuration reads this state via a remote-state data source, it must be able to decrypt it. Account for cross-config dependencies.
  • It’s an OpenTofu feature. Terraform proper doesn’t have native client-side state encryption in the same form. This is genuinely a reason teams cite for the switch — but verify it fits your tooling before betting on it.

Is it worth it?

If your state holds anything sensitive — and it almost certainly does — and your compliance posture cares about defense-in-depth, client-side encryption removes an entire class of “the bucket was readable” incident. The migration is a one-time cost with a clear fallback path, and the day-to-day overhead is essentially zero.

Encryption config is exactly the kind of change where a small mistake (a removed fallback before migration completes, a key only one person can access) becomes an outage, so it’s worth a careful review before merge. Running it through a code review workflow catches the lockout traps. For more on hardening state, see our other Terraform guides.

State encryption behavior and key providers differ across OpenTofu versions. Verify against your current release and test the migration in a non-production state before touching prod.

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.