# Changelog 2026-07-10 -- OpenTofu state file security posture (DOCFIX-175)

No live infrastructure touched -- no `tofu` binary has ever been run in any
session that authored this repo clone, so no `terraform.tfstate` exists yet.
This is a preventive fix, found while reviewing what's genuinely missing from
the VR1 buildout ahead of Stage 1's own first real `tofu apply`
(`runbooks/dc-dc-phase0-vcloud-prep.md` Step 10 -- DC1 planes + Office1 pool
+ mesh links), not a live incident.

**Correction made mid-delivery, worth recording:** the first draft of this
work assumed Stage 3 (`dc-dc-phase2-tofu-dc-substrate.md`) was the first real
apply, based on that file's name suggesting it was "the OpenTofu stage."
Reading the actual runbooks directly (not inferring from filenames) showed
Stage 1's own Step 10 applies DC1's planes, Office1's storage pool, and the
three mesh links -- exactly what's already uncommented in `opentofu/main.tf`
today -- well before Stage 3 touches anything. Caught and fixed before
committing, same "verify against the actual file, not the name" discipline
this repo already uses everywhere else.

## The gap

Nothing in this repo documented how OpenTofu's own state file is protected.
No `backend` block is configured anywhere (`versions.tf`/`main.tf` are silent
on it; every stage's own `tofu init` runs `-backend=false`), so state
defaults to a local `terraform.tfstate` in whatever directory `tofu` is
invoked from (the Office1 operator VM, per `opentofu/README.md`'s own
framing). Two real problems follow from that:

1. **Plaintext secrets in state.** `var.maas_api_key` is marked
   `sensitive = true`, but that flag only redacts the value from CLI/plan
   OUTPUT -- verified against HashiCorp's own current documentation (not
   assumed): *"Terraform stores values with the `sensitive` argument in both
   state and plan files, and anyone who can access those files can access
   your sensitive values"* (developer.hashicorp.com/terraform/language/
   manage-sensitive-data, fetched 2026-07-10). This applies regardless of
   whether the value is consumed by a resource argument or, as here, only by
   a `provider {}` block -- the docs draw no such distinction. At Stage 1
   time `maas_api_key` may still be a placeholder (MAAS doesn't exist for
   real until Stage 2 -- see Stage 1's own "Known gap" section), but the
   moment a REAL `TF_VAR_maas_api_key` is exported for any later plan/apply,
   it lands in state in the clear, with no protection beyond filesystem
   permissions -- and nothing documented what those permissions should be.
2. **State was not gitignored.** Harmless only by accident (no state file has
   ever existed in a repo clone), not by design -- a real gap that would have
   let a future `git add -A` commit a plaintext-credential-bearing file to
   history.

Neither problem was previously logged anywhere -- not the gap register, not
`opentofu/README.md`, not `.gitignore`.

## Fix

1. **`.gitignore`**: added `opentofu/**/*.tfstate`, `*.tfstate.*`,
   `.terraform/`, `*.tfplan`, `*.auto.tfvars*`, and the standard Terraform
   override/crash-log patterns. `.terraform.lock.hcl` deliberately NOT
   excluded (provider-version pin; matches this repo's own exact-release-not-
   range discipline already established in `versions.tf`).
2. **`opentofu/variables.tf`**: `maas_api_key`'s description now states the
   plaintext-in-state caveat inline and directs to `TF_VAR_maas_api_key`
   (never `-var`, which lands in shell history) -- consistent with, and
   reinforcing, `dc-dc-phase0-vcloud-prep.md` Step 7's own pre-existing
   "never commit `maas_api_key`" guidance for the *input* side; this delivery
   covers the *output* (state) side that guidance didn't.
3. **`opentofu/README.md`**: new "State file handling" section covering the
   risk and five concrete mitigations for VR1's rehearsal scope (never
   commit state, never pass secrets via `-var`, restrict file permissions,
   back up out-of-band per the existing `~/vault-init/`-class secret
   discipline, and a flagged-not-decided note that local state has no
   locking -- a real Roosevelt/production gap, not solved here).
4. **`runbooks/dc-dc-phase0-vcloud-prep.md`**: Step 10 (the actual first real
   `tofu apply` in this whole repo) gained a "SECURE THE STATE FILE" callout
   immediately after it -- `chmod 700`/`chmod 600`, a `git status` check that
   the state file shows ignored not staged, out-of-band backup, and an
   explicit note that this is a standing check repeated after every
   subsequent stage's apply, not a one-time Stage 1 step.
5. **`runbooks/dc-dc-phase2-tofu-dc-substrate.md`**: Step 8 gained a short
   pointer back to Stage 1's callout (not a full duplicate) plus a note that
   Step 9 next is where this stage first needs a REAL MAAS credential to
   succeed.
6. **`docs/dc-dc-deployment-workflow.md`**: added gap-register item #18
   (this gap, CLOSED) and #19 (a related, still-OPEN gap found in the same
   review pass: no teardown/rollback runbook exists for the VR1 OpenTofu
   layer itself -- tracked separately, not invented as a rushed addendum
   here).

## What this deliberately does NOT do

Migrate to a remote backend (S3-compatible + lock table, Terraform Cloud,
Consul, etc.) or add real state locking. Both are production-grade
infrastructure decisions appropriate for Roosevelt planning, not something to
invent unilaterally for a rehearsal repo that has never run `tofu` for real.
Flagged explicitly in the README rather than silently deferred.

## Verification

`bash scripts/repo-lint.sh`: 0 fail, 1 documented legacy warn. No script
logic changed -- `.gitignore`, one `.tf` file (description-only edit, no
resource/variable type change), one README, two runbooks (prose additions),
one tracker doc. No test harness applies (nothing here is executable script
logic); `git diff --stat` confirms no `.sh`/`.py` file in this delivery.

REVERT: `git checkout HEAD~ -- .gitignore opentofu/variables.tf
opentofu/README.md runbooks/dc-dc-phase0-vcloud-prep.md
runbooks/dc-dc-phase2-tofu-dc-substrate.md
docs/dc-dc-deployment-workflow.md` (safe -- no live infrastructure depends on
any of these; nothing here has ever been applied against real state).

## Next actionable step

Gap #19 (VR1 teardown/rollback runbook) is a separate, tracked follow-up --
see its own changelog once delivered.
