Newer
Older
openstack-caracal-dc-dc / docs / changelog-20260710-opentofu-state-file-security.md

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.