# how to store Terraform state securely

## TL;DR
Keep state in a remote backend like S3 with encryption at rest, versioning on, and a lock (DynamoDB table or the backend's native locking) so two applies cant corrupt it. Lock the bucket down with a tight IAM policy and never commit state files to git, because state routinely contains secrets in plaintext. Treat read access to state like read access to production credentials.

## The query
```text
how to store Terraform state securely
```

## Use this when
- you are setting up a Terraform backend for a team for the first time
- someone asks "where should we keep tfstate" or "is local state safe"
- you need state locking so concurrent applies dont corrupt state
- you are auditing who can read or write the state bucket

## Not for this skill when
- you are debugging a specific terraform apply error (fix the config first)
- you need secrets management for apps (use a real secrets manager, state is not one)
- you are migrating between backends mid-project (that needs a careful state mv plan)

## Steps
1. Create the state bucket with versioning and default encryption enabled, and block all public access on it.
   Expected output: bucket settings show versioning Enabled, default encryption on, public access blocked.
2. Add a lock: create a DynamoDB table with a `LockID` string hash key, or use the backend's native locking if your Terraform version supports it, and reference it in the backend block.
   Expected output: a second concurrent `terraform apply` waits or errors instead of corrupting state.
3. Write the backend block in your root module with the bucket name, key path, region, encryption on, and the lock table name. Never put credentials in the block; pass them via environment or the CLI config.
   Expected output: `terraform init` reports successful backend initialization.
4. Restrict IAM: allow state read and write only to the CI role and the few humans who run applies. No wildcard principals, no public access.
   Expected output: an access review shows a short list of principals with GetObject and PutObject on the state prefix only.
5. Remove local state from git: add `*.tfstate` and `*.tfstate.backup` to .gitignore, and purge any state files already committed (rotate any secrets they contained).
   Expected output: `git status` no longer shows state files, and a history scan finds none.
6. Enable CloudTrail data events or bucket access logging on the state bucket so you can see who read state and when.
   Expected output: log entries appear for each state read and write.

### Variant: Terraform Cloud or HCP Terraform remote backend
If you use the hosted backend, the workspace stores state encrypted for you and locking is built in. Your job shrinks to scoping team permissions on the workspace and turning on SSO.

### Variant: Azure or GCP backends
Same principles: Azure Blob Storage with a container plus blob leasing for locks, or GCS with versioning and uniform bucket-level access. Encryption at rest, tight IAM, no state in git.

### Variant: one state per environment
Split state by environment (dev, staging, prod) with separate keys or workspaces. A bad apply in dev should never be able to touch prod state.

## Why this happens
State files mirror your real infrastructure, including database passwords, private keys, and tokens that providers store in plaintext. Local state on a laptop is one stolen machine away from a full credential leak, and unlocked remote state is one concurrent apply away from corruption.

## Edge cases and pitfalls
- State locking is not a substitute for reviewing plans. Always read the plan before apply.
- Old state versions in a versioned bucket still contain old secrets. Lifecycle-expire them or rotate secrets after a leak.
- `terraform_remote_state` data sources widen read access. Scope them to exactly the outputs needed.
- Migrating backends rewrites where state lives. Back up the old state first and verify the new backend reads it before deleting anything.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_b4DikU99rIT2ygOJMz2qbg
