Skip to content
Hogin Hogin
Go back

OpenTofu: state encryption and leaving Terraform without drama

9 мин чтения

Open a terraform.tfstate from any project older than a year and you’ll almost certainly find database passwords, private keys, and API tokens sitting in plaintext. It’s always been this way: state is a dump of everything Terraform knows about your infrastructure, including sensitive-marked variable values. OpenTofu — the Terraform fork — got something in 2024 that the original still lacks: client-side encryption of both state and plan. And migrating from Terraform takes an hour, not a sprint.

Table of contents

Open Table of contents

OpenTofu vs Terraform, briefly

OpenTofu was born when HashiCorp switched Terraform’s license from MPL 2.0 to BSL in 2023 — the code stayed readable, but using it in competing products got banned. The Linux Foundation forked the last MPL version, and OpenTofu has since lived as a community-governed project rather than one owned by a single company.

In practice that means three things:

Of all the new features, state encryption is the strongest argument for migrating — it closes a gap teams have lived with for years, and it does so without bolting on third-party tooling.

The problem: state is a plaintext secrets dump

Terraform state stores every resource attribute, including ones marked sensitive. The sensitive flag only hides a value in CLI output — in the actual state JSON it sits in plaintext. The same goes for plan files, which teams routinely save as CI artifacts to feed a separate apply job.

Backends — S3, Azure Blob, GCS, Terraform Cloud — usually encrypt data at rest at the storage layer. But that encryption is transparent to anyone with read access to the object: S3 decrypts the content automatically before returning it, as long as the caller has s3:GetObject. So the real security perimeter is the bucket’s IAM policy, not crypto. Leak the bucket credentials, and the whole state leaks in the clear.

Who can read state: the backend encrypts the disk but still hands out plaintext by IAM right

Client-side encryption: how OpenTofu does it

Since 1.7, OpenTofu can encrypt state and plan before the data leaves for the backend, and decrypt them after pulling them back locally. In this scheme the backend only ever sees ciphertext — even an attacker with full read access to the S3 bucket gets a meaningless blob without the key.

Configuration lives in an encryption block inside terraform {} and has three parts: a key provider (where the key material comes from), a method (the algorithm — in practice always aes_gcm), and a binding of that method to state/plan:

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

    method "aes_gcm" "main" {
      keys = key_provider.pbkdf2.main
    }

    state {
      method = method.aes_gcm.main
    }

    plan {
      method = method.aes_gcm.main
    }
  }
}

There are several key providers, each covering a different scenario:

The whole file gets encrypted — not individual fields, but the entire state or plan JSON, metadata included. That’s why, once encryption is on, the backend holds an unreadable binary blob rather than a state file with a few fields blacked out.

Encryption flow: key provider → method → encrypted state at the backend

Backend encryption is not the same thing

The confusion comes up constantly: “we’ve got SSE-KMS on our S3 bucket, isn’t that enough?” No, and the reason is fundamental.

Backend encryption (SSE-S3, SSE-KMS, Azure Storage Service Encryption) protects against disk theft — a stolen physical medium or a storage snapshot pulled outside the API. It protects nothing against a legitimate read request: if the caller holds the IAM right, the backend decrypts and hands back plaintext on its own. That’s encryption of transport and storage, not encryption of the data for the recipient.

OpenTofu’s client-side encryption protects the content itself: state is encrypted before it’s sent and stays ciphertext for anyone without the key — including a bucket admin who lacks rights on the KMS key. The two layers address different threats and work well together: SSE closes off physical access to the disk, client-side encryption closes off excessive IAM grants and logs that state might have leaked into.

Comparison

Backend encryption (SSE)OpenTofu client-side encryption
What it encryptsthe disk/object in storagethe state/plan JSON itself
Who sees plaintextanyone with an IAM right on the backendonly the holder of the encryption key
Transparency to a readerfull (auto-decrypted)none without the key
Configured atthe backend side (S3/Azure/GCS)the OpenTofu side, encryption block
Protects againstdisk/snapshot theftexcess IAM rights, logs, prying eyes at the backend
Present in Terraformyesno

What you need to encrypt an existing state file

The project already has a state file describing real infrastructure — you need to switch it to encrypted mode without downtime and without risking the data. The mechanism is fallback: OpenTofu tries the primary (encrypting) method, and if reading fails, falls back to a secondary one.

First, add encryption with a fallback for reading the old plaintext state:

terraform {
  encryption {
    key_provider "aws_kms" "main" {
      kms_key_id = "alias/tofu-state"
      region     = "eu-central-1"
      key_spec   = "AES_256"
    }

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

    method "unencrypted" "migrate" {}

    state {
      method = method.aes_gcm.main
      fallback {
        method = method.unencrypted.migrate
      }
    }
  }
}

Next, a single tofu apply (or even tofu refresh) does it: OpenTofu reads the current plaintext state through the fallback, and writes back an encrypted version using the primary method. After that, verify on the backend that the file is genuinely binary, then remove the method "unencrypted" "migrate" block along with fallback — there’s no point keeping it, it no longer buys you any backward compatibility.

How to verify secrets are no longer in plaintext

The most reliable check is looking at the object in the backend by hand, rather than trusting the config:

aws s3 cp s3://my-bucket/env/prod/terraform.tfstate - | file -
# expected: data (not ASCII text, not JSON)

aws s3 cp s3://my-bucket/env/prod/terraform.tfstate - | grep -ai "password\|secret"
# expected: nothing — before migration this would have found matches

If file recognizes valid JSON instead of binary data, encryption either didn’t apply, the config wasn’t picked up, or state is still being written by the old terraform binary instead of tofu. Check this before treating the migration as done.

Pitfalls

Bottom line

State encryption is a rare case in IaC where the entry cost is close to zero and the gap it closes is years old and real: secrets that sat in plaintext at a backend reachable by IAM right, not by actual key ownership. Migrating from Terraform to OpenTofu for this one feature alone is justified on its own — HCL and state-level compatibility make the move a one-day task, not a project. If your state isn’t encrypted client-side anywhere yet, this is the cheapest security win available this week.


Share this post:

Next Post
Istio ambient: a service mesh without sidecars