State files store secret data such as passwords and API keys in plaintext in the remote backend. This episode covers OpenTofu's exclusive feature: client-side state encryption before it's sent to storage, complete with AWS KMS, GCP KMS, and PBKDF2 passphrase key providers.

In the previous episode 5 we covered remote state management and state locking — how the state file moves from a laptop to centralized storage like S3, GCS, or an HTTP backend, complete with a locking mechanism so two people don't apply at the same time. State lives in the cloud, collaboration gets easier, and conflicts decrease. But there's one big problem we deliberately haven't mentioned yet: the state content itself isn't secure.
Try to remember what's stored inside opentofu.tfstate. It's not just resource IDs like i-0a1b2c3d, but also all the post-apply attributes — and that's where the danger is. When a database auto-generates a password, or a module stores a secret value as an attribute, that value gets embedded in the state in plaintext. Anyone who can read your S3 bucket — or a leaked state backend — can automatically read all those secrets.
In this episode we cover OpenTofu's first exclusive feature that sets it apart from Terraform: native client-side state encryption. You'll see how state is encrypted on the client side before being sent to storage, learn the structure of the encryption block, and get to know the available key provider options.
Many engineers treat the state file as an "internal technical detail" and put it in an S3 bucket without extra protection. Yet behind the innocuous-looking resource IDs, state stores sensitive data:
Warning
Access control on the S3 bucket is important, but it's not enough. Remote state backends are read automatically by CI machines, other tooling, and are sometimes backed up to unmonitored locations. You can't rely on "who can open the bucket" alone — the data must be secure even while sitting in storage you don't fully control.
Terraform has long relied on two defenses: sensitive = true to hide values from output and logs, plus tight backend access. Both hide information from human eyes, but the file in storage is still stored plain — anyone who steals the file gets everything immediately. That's where OpenTofu goes further.
Native client-side state encryption works on a simple principle: data is encrypted on the machine running tofu plan or tofu apply, and only ciphertext is sent to the remote backend. Storage only sees a blob of random bytes — even if the bucket leaks, nothing can be read without the key.
The concept is like a diary kept in a bank vault: the vault (S3 bucket) only stores the book already locked with a padlock, while the person who opens the padlock holds a key you keep separately.
The configuration architecture splits into three layers:
| Layer | Role |
|---|---|
key_provider | The encryption key source — where the key comes from (KMS, passphrase, etc.) |
method | The encryption algorithm used (e.g. AES-GCM) plus which key provider's key |
target (state / plan) | The data portion protected — state file, plan file, or both |
encryption BlockLet's look at a full configuration for encrypting the state file using AWS KMS:
encryption {
key_provider "aws_kms" "main" {
kms_key_id = "arn:aws:kms:ap-southeast-1:123456789012:key/xxxx"
}
method "aes_gcm" "main" {
keys = key_provider.aws_kms.main
}
state {
method = method.aes_gcm.main
}
}Read this configuration from the bottom up: the state target uses method.aes_gcm.main, that method takes its key from key_provider.aws_kms.main, and the key provider requests the key from AWS KMS. Every time OpenTofu writes state, it calls AWS KMS to get a data key, encrypts the state with AES-GCM, then sends the result to the backend.
Once this block is active, try opening the state in S3 via the console — what you'll see isn't a pretty JSON with resources, but an encrypted blob. Running tofu plan and tofu apply stays exactly the same; your workflow doesn't change at all.
Note
The encryption block can also apply to plan files by adding a plan { method = method.aes_gcm.main } block inside it. The benefit: plan files from tofu plan -out=plan.tfplan — which are often stored in CI caches — get protected too.
OpenTofu designed the key_provider abstraction so you can choose your key source according to your ecosystem:
| Key Provider | Key Source | Best For |
|---|---|---|
aws_kms | AWS KMS Customer Managed Key | Teams fully on AWS |
gcp_kms | Google Cloud KMS | Teams running on GCP |
azure_keyvault | Azure Key Vault | Teams in the Azure ecosystem |
pbkdf2 | Passphrase derived into a key | Solo developers or small teams |
openpgp | OpenPGP public key | Cases without a cloud KMS |
external | Custom external program | HSM or internal vault integration |
For small teams without KMS yet, the pbkdf2 provider is the easiest path — just one passphrase that OpenTofu turns into an AES key through PBKDF2 iteration:
encryption {
key_provider "pbkdf2" "my_passphrase" {
passphrase = "replace-with-a-strong-passphrase"
}
method "aes_gcm" "main" {
keys = key_provider.pbkdf2.my_passphrase
}
state {
method = method.aes_gcm.main
}
}Warning
A lost PBKDF2 passphrase means the stored state cannot be recovered — there's no built-in recovery mechanism because encryption happens before data leaves the client. Store the passphrase in the team's password manager and combine it with backups of older state versions. For production, cloud KMS is more recommended because its keys are centrally managed and can be rotated.
This feature is purely exclusive to OpenTofu — in Terraform, state is always stored as-is in the backend. After migrating, you simply add an encryption block and run tofu plan and tofu apply; old state gets encrypted automatically on the next write, with no manual migration needed.
Recommended practices:
In episode 6 we built the state security foundation in OpenTofu:
encryption block consists of three layers: key_provider (key source), method (algorithm like AES-GCM), and target (state / plan).This is the best layered defense for one of the most important assets in IaC. In the next episode, episode 7, we'll cover OpenTofu's second exclusive feature: Dynamic Provider Iteration — loading many provider instances dynamically using for_each to manage multi-region and multi-account setups with concise code. See you in the next episode!