← Back to Blog
· 7 min read · API Stronghold Team

Stop Encrypting tfstate. Start Expiring the Secrets Inside It.

Cover image for Stop Encrypting tfstate. Start Expiring the Secrets Inside It.

Every time you run terraform apply, Terraform writes a snapshot of your infrastructure to a state file. That file is how Terraform tracks what it created, what needs updating, and what should be destroyed. It’s also, quietly, a complete inventory of every secret your infrastructure has ever touched.

That includes database passwords. API keys. IAM access key IDs and secret access keys. Generated tokens. TLS private keys. Anything your provider or resource module returns as output ends up in terraform.tfstate, in plaintext JSON.

This isn’t a bug or an edge case. It’s how Terraform works. And for most teams running production infrastructure, it’s one of the most underappreciated credential exposure vectors in their stack.

What Actually Lives Inside a tfstate File

Terraform state is just JSON. Open any non-trivial terraform.tfstate and you’ll find a resources array containing every managed resource, its full attribute set, and all computed values.

Here’s a condensed example of what an AWS RDS instance looks like in state:

{
  "type": "aws_db_instance",
  "values": {
    "identifier": "prod-postgres",
    "username": "admin",
    "password": "xK9mR2pLwN7qT4vB",
    "endpoint": "prod-postgres.abc123.us-east-1.rds.amazonaws.com",
    "port": 5432
  }
}

That password field? Plaintext. Not hashed. Not encrypted at the field level. Just sitting there.

The same applies to IAM access keys provisioned via aws_iam_access_key:

{
  "type": "aws_iam_access_key",
  "values": {
    "id": "AKIAIOSFODNN7EXAMPLE",
    "secret": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "user": "deploy-bot"
  }
}

Both the key ID and the secret are there. If an attacker gets this file, they have working AWS credentials, no brute force required.

Provider configurations, API tokens passed as resource arguments, generated certificate private keys, random passwords from the random_password resource, SSH private keys from tls_private_key resources: all of it goes into state.

The sensitive = true Trap

Terraform 0.14 introduced the sensitive attribute flag, and a lot of developers assumed it meant their secrets were protected. It doesn’t work that way.

Marking a value as sensitive = true does two things: it redacts the value in terraform plan and terraform apply output (showing (sensitive value) instead), and it prevents the value from being passed as a non-sensitive output without explicit acknowledgment. That’s it.

The value is still written to state in plaintext. The sensitivity flag is purely a UI-layer concern. It has zero effect on what lands in terraform.tfstate or your remote backend.

This matters because teams sometimes decide not to rotate keys provisioned by Terraform because “we marked them sensitive.” That’s a false sense of security. The keys are in state regardless.

How Attackers Get to Your State Files

State files end up in attacker hands through a handful of predictable paths.

S3 backend misconfiguration is probably the most common. The default for an S3 bucket used as a Terraform backend is private, but bucket policies get changed, overly permissive IAM roles get attached, and sometimes someone sets up public access during troubleshooting and forgets to revert it. A single misconfigured bucket policy can expose every state file across every workspace.

Terraform Cloud and Enterprise access tokens are another entry point. Terraform Cloud stores state centrally. If a developer’s API token is compromised (leaked in a GitHub repo, stolen from a compromised workstation, left in a shell history file), an attacker can use the Terraform Cloud API to download state directly. One token, all the state.

CI/CD pipeline secrets create similar exposure. Your CI system needs credentials to run Terraform. Those credentials often have broad permissions. If an attacker can inject into your pipeline (malicious dependency, compromised runner, stolen CI token), they can run terraform state pull and get everything.

Git commits are still happening. Local terraform.tfstate files get accidentally committed to repositories. Even when caught and removed from history with git filter-repo, secrets often persist in forks, clones, or CI artifact logs. GitHub’s secret scanning helps, but it’s a catch-up game.

The Blast Radius Problem

The thing that makes state file exfiltration painful isn’t just that your current secrets are exposed. It’s that state tracks the full history of what Terraform has managed.

When an attacker gets your tfstate, they often get credentials for things you’ve already decommissioned. Old IAM keys that were never rotated after being removed from Terraform management. Database passwords from instances that were replaced but whose secrets were reused. Keys you thought were gone because the resource no longer exists in your config.

Terraform doesn’t scrub secrets from state after resources are destroyed. Once a value was written to state, it stays in the state history until you explicitly prune it or the backend’s versioning purges it.

This means a stale state file from six months ago could still contain valid credentials if those credentials were never rotated. The blast radius isn’t bounded by what you’re running today. It extends back through the full lifecycle of your infrastructure.

A compromised state file can easily hand an attacker credentials to multiple cloud accounts, several databases, external API integrations, and internal service tokens. All from a single JSON file.

Your tfstate has credentials an attacker can use right now

Short-lived, scoped credentials expire before they can be weaponized. Even if your state file is exfiltrated, the keys inside are already gone.

No credit card required

Short-Lived Credentials Change the Math

The standard advice for state file security is to encrypt your backend, restrict access, enable versioning, and rotate keys. All of that is correct. None of it makes the problem go away.

Encryption at rest protects state from someone with raw storage access, but not from someone with valid cloud credentials or a compromised CI token. Restricted access policies reduce the attack surface, but misconfigurations happen. Key rotation helps, but manually rotating every credential that’s ever appeared in state is practically impossible at scale.

Short-lived, scoped credentials attack the problem differently. If the API keys in your state file have a 15-minute TTL, an attacker who exfiltrates that file gets credentials that are already expired. There’s no window to pivot.

This is the core argument for dynamic credentials and phantom tokens in Terraform-managed infrastructure. Vault’s AWS secrets engine, for example, generates IAM credentials on demand and revokes them automatically after a configured TTL. Those credentials still end up in state, but by the time anyone reads the state file (other than Terraform itself during the apply window), the keys are gone.

The same principle applies at the API integration layer. Instead of provisioning long-lived API keys and storing them in Terraform state, use short-lived tokens scoped to the specific operation. When a credential has a one-hour lifetime and no refresh path, exfiltrating it from state gives an attacker at best a narrow window, often already closed.

This doesn’t eliminate state file security as a concern. It changes what an attacker gets. An expired credential is useless. A long-lived key with broad permissions is a full account compromise.

Practical Steps to Reduce Your Exposure

Start with an audit of your remote backend access controls. For S3 backends, review your bucket policy, block public access settings, and the IAM roles that have s3:GetObject on the state bucket. Scope that access tightly. Terraform execution roles and CI systems should have the minimum permissions to read and write state, nothing more.

Enable state locking. DynamoDB-based locking for S3 backends prevents concurrent state modifications, which also limits the window during which state can be inconsistently read. It’s not a security control per se, but it narrows the race condition surface.

Review what’s actually in your state file. Run terraform state list and check which resource types you’re managing. Pay attention to anything that generates credentials: aws_iam_access_key, random_password, tls_private_key, google_service_account_key, and similar resources. Those are your highest-risk entries.

Rotate any long-lived keys that appear in state, especially if you can’t confidently say the state file has never been inappropriately accessed. Assume compromise, rotate, move on.

Where possible, replace long-lived provisioned credentials with dynamic ones. HashiCorp Vault’s secrets engines for AWS, GCP, Azure, and databases can replace static key provisioning with on-demand credential generation. Some cloud providers offer native alternatives: AWS IAM Roles for EC2 and ECS, Workload Identity Federation for GCP, Azure Managed Identities. These don’t generate secrets that end up in state at all.

For external API integrations, the same logic applies. If you’re provisioning API keys through Terraform and storing them for long-term use, evaluate whether a short-lived token model is available. Many API providers now support OAuth 2.0 client credentials flows or token exchange mechanisms that reduce credential lifetime without increasing operational friction.

Finally, check your .gitignore. Every Terraform repo should ignore terraform.tfstate and terraform.tfstate.backup locally. If you’re using a remote backend (which you should be), there’s no reason for state to ever touch your filesystem during normal operations.

State file security isn’t glamorous infrastructure work. But it’s one of the highest-leverage things you can do to reduce your blast radius, and it costs almost nothing compared to cleaning up after a credential compromise.

Expired credentials are worthless credentials

API Stronghold issues short-lived, scoped tokens for your infrastructure integrations. By the time someone reads your tfstate, the credentials inside are already gone.

No credit card required

Keep your API keys out of agent context

One vault for all your credentials. Scoped tokens, runtime injection, instant revocation. Free for 14 days, no credit card required.

Get posts like this in your inbox

AI agent security, secrets management, and credential leaks. One email per week, no fluff.

Your CI pipeline has permanent keys sitting in env vars right now. Scoped, expiring tokens fix that in an afternoon.

One vault for all your API keys

Zero-knowledge encryption. One-click sync to Vercel, GitHub, and AWS. Set up in 5 minutes — no credit card required.