← Back to list

A Decade in the State File

On October 28, 2014, someone opened GitHub Issue #516 on the Terraform repository. The request was simple: stop writing secrets in…

Nicholas Browdues in Platform Engineering Labs · 2026-06-02 20:09 · 10 claps · 5.8 min read
#terraform #devops #infrastructure-as-code #cybersecurity #formae
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🔒 · Cybersecurity 🔓 · Open Source

A Decade in the State File

On October 28, 2014, someone opened GitHub Issue #516 on the Terraform repository. The request was simple: stop writing secrets in plaintext to state files. The author had been keeping Terraform state in version control publicly, and had just realized Terraform was recording database passwords there in the clear.

The issue stayed open for more than ten years, and not because nobody noticed. The original post collected more than five hundred thumbs-up, and the thread accumulated a long tail of comments and related issues, with the same discovery repeating at regular intervals: someone would mark a value as sensitive, inspect their state, and realize the secret was still there. The terminal output was quieter. The state file was not.

Over a whole decade, the issue outlasted teams, companies, and entire shifts in how the industry thinks about infrastructure.

In 2025, HashiCorp finally closed it. The solution was write-only arguments, built on the ephemeral values introduced the release before, and it was real progress. Ephemeral values give Terraform a way to use a value without persisting it to state or plan files. Write-only arguments give providers a way to accept those values without storing them back into Terraform’s record of the world.

But the shape of the fix matters. It only works where the provider and the individual resource have been updated to support it. For each resource that handles a secret, the provider has to expose a write-only variant of the relevant argument, usually a _wo field. Coverage is therefore provider-specific, resource-specific, and easy to fall out of once you leave the happy path.

The follow-up issues tell the story. In one case, an engineer tried to pass an ephemeral AWS Secrets Manager value into an Auth0 resource and hit a wall, because the resource had no write-only field that could accept it. The proposed fix was to add _wo properties to that provider’s resources. None of this is a dunk on one provider. It is the ecosystem cost of a decision made lower in the stack, and it explains why a fix that finally shipped still arrives one resource at a time.

What the sensitive flag actually does

Terraform’s own documentation has said the quiet part for years: state can contain sensitive values, including initial database passwords, and local state is stored as plaintext JSON. The recommendation is to treat state itself as sensitive data. Yet many people believe that marking a variable sensitive protects it, and it does not. The flag hides values from normal CLI output during plan and apply. It does not, by itself, keep those values out of Terraform state. Unless a value flows through one of Terraform’s newer omit-from-state paths (such as ephemeral values plus a supported write-only arg), Terraform still needs to remember it, and for local state that means plaintext JSON on disk.

Remote state improves the story without making it disappear. HCP Terraform encrypts state at rest, and backends like S3 or GCS can be configured with encryption and access controls, but the secret is still persisted inside the state or plan artifact, and anyone or anything with sufficient access to that artifact can read it. The real boundary was never whether the terminal printed the password. It was whether the infrastructure tool remembered it. And the exposure is not theoretical. State files get committed to git, copied into CI/CD artifacts, exported for debugging, and dropped into buckets with border access than intended. When the state is available as JSON, the credentials are not hidden behind application behavior or specialized tooling. They are simply there.

The same shape, twice

Secrets in state is not the only place this pattern shows up. Terraform’s language went through a similar evolution. In the early Terraform world, iteration was not a first-class part of the model. If you wanted many similar resources, you reached for count, indexes, splat expressions, and careful list bean-counting. More expressive constructs like for expressions and for_each arrived later, in the Terraform 0.12 / HCL2 era, after the language had to be substantially reworked. HashiCorp’s own Terraform 0.12 preview described years of users building clever but unintuitive workarounds around the language’s limits. Five years to make iteration first-class, more than ten to give secrets a state-free path: different features, same shape.

The point is not that nobody cared. The point is that early abstractions become public surface area. Public surface area becomes ecosystem contract.

And once enough tooling, providers, modules, workflows, and habits grow around that contract, even obvious changes have to thread their way through everything built on top. That is what makes Issue #516 worth reading. What stands out is how long the problem lived inside the shape of the tool.

Where the seam was drawn

Compare how the parallel project handled the same class of problem. OpenTofu made client-side state encryption a headline feature of its 1.7.0 release in 2024, with a straightforward idea: encrypt the state and plan artifacts before they land in the backend. That does not remove every security concern. You still have to protect keys, and still have to care about operators, access, automation, and recovery. But it addresses the artifact at the layer where the artifact lives.

Terraform’s newer path works the other way around. It creates a way for selected values not to enter the artifact in the first place, but only when Terraform core, the provider, and the individual resource schema all agree on how to handle them. One approach protects the state artifact. The other teaches more and more resources how not to write certain values into it. The difference is not effort, it is where the seam was drawn.

Why we build formae on a database

I read through Issue #516 (more than 10 years of it!) and what stayed with me was not the bug. It was watching a decision made for one set of conditions get carried into a completely different set. Terraform state made sense in the early Terraform world: a local workflow, a small team, a single JSON document with everything in it, simple and portable and powerful. Then the world around it changed. Teams got bigger, backends became shared, CI/CD became the default, and state moved into buckets, workspaces, artifacts, and automation systems. Secrets moved with it. Once they entered that model, the security story expanded to include every backend, every CI job, every teammate with state access, every exported plan, and every accidental copy.

So we built **formae** on a datastore model instead of a Terraform-style state file, not as an optimization, but as a starting point.

Resource state is datastore-agnostic by design in formae. In principle, it can live in any datastore with the right adapter.

Today, formae uses SQLite by default, with support for Postgres and Microsoft SQL Server. The important distinction is not “database good, file bad.” SQLite is still a file. Postgres still has to be secured. The distinction is that formae does not treat infrastructure state as a flat JSON artifact that is meant to be copied around, uploaded somewhere, locked externally, and protected by convention. State is part of the system, so secrets have to be treated differently at the boundary.

Where Terraform’s sensitive flag hides a secret from your terminal while still allowing it to be stored in state, formae takes a different path. Mark a value as opaque in your Pkl config and formae uses the plaintext only for the cloud operation, because the cloud provider still needs the real value, but the raw value is never persisted. The datastore keeps a SHA-256 hash so formae can detect changes without storing the secret itself. The CLI shows opaque value changed — rather than the value. No Terraform-style plaintext JSON state file carries the raw secret forward, and even the database does not hold the raw secret.

Command will
└── update resource DBSecret1
    ├── of type AWS::SecretsManager::Secret
    ├── in stack secret-resolution1
    └── by doing the following:
        └── set property "SecretString" (opaque value)

This operation will update 1 resource(s).

Do you want to continue? (Y): Y

That is the design choice. Not a patch on top of a file format, and not a convention that depends on every downstream provider remembering to opt in, but a different starting point: state as a managed system, and secrets as values that should pass through the tool, not settle inside it.

formae is built in the open. Check it out


메타데이터
post_id
dc1f1cb08bd1
slug
a-decade-in-the-state-file-dc1f1cb08bd1
url
https://blog.platform.engineering/a-decade-in-the-state-file-dc1f1cb08bd1
canonical_url
https://blog.platform.engineering/a-decade-in-the-state-file-dc1f1cb08bd1
author_url
https://medium.com/@nicholas.browdues
status
ok
fetched_at
2026-06-09 15:37:30