Skip to content

feat(infra): OpenTofu 1.12 + defense-in-depth VPS protection - #166

Merged
agjs merged 2 commits into
mainfrom
infra/opentofu-1.12-prevent-destroy
Jun 11, 2026
Merged

feat(infra): OpenTofu 1.12 + defense-in-depth VPS protection#166
agjs merged 2 commits into
mainfrom
infra/opentofu-1.12-prevent-destroy

Conversation

@agjs

@agjs agjs commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Context

Goal: make production hard to nuke, and require an explicit action to do it — defense in depth against accidents. The single Hetzner VPS (module.hetzner.hcloud_server.main) carries every persistent Docker volume (Postgres data, acme.json, GlitchTip), so losing it is catastrophic. OpenTofu 1.12 lets prevent_destroy reference input variables, which we use to build a single, explicit "protect" switch across two independent layers.

Two protection layers (one switch)

Both are driven by the root var prevent_server_destroy (bool, default true):

  1. OpenTofu layerlifecycle.prevent_destroy on the server. Rejects any plan that would destroy or -replace it. Because a blanket tofu destroy aborts entirely if any resource is protected, this also transitively shields the DNS records + DNSSEC at the tofu layer.
  2. Hetzner API layerdelete_protection + rebuild_protection on the server. Enforced by Hetzner's API regardless of tooling: blocks deletion/rebuild from the Hetzner console, hcloud CLI, raw API, or a tofu run with broken state — not just a plan through this config.

Layer 1 alone only protects tofu runs through this exact config + state. Layer 2 closes that gap.

Deliberately rebuilding the host (explicit, two-step)

tofu apply -var prevent_server_destroy=false                                   # lifts both locks
tofu apply -var prevent_server_destroy=false -replace=module.hetzner.hcloud_server.main   # rebuild

The API lock must be lifted by an apply before a destroy can succeed, which is exactly the accident-resistance we want.

Other changes

  • Version floormain.tf: required_version >= 1.6.0>= 1.12.0 (dynamic prevent_destroy errors on older versions).
  • CI — pin opentofu/setup-opentofu to tofu_version: "1.12.0" (was unpinned/latest).
  • Docsterraform.tfvars.example documents the single toggle; the server's inline comment documents both layers and the rebuild path.
  • Lockfile.terraform.lock.hcl gains the full set of cross-platform h1: checksums that 1.12's improved checksum handling adds on init (same provider versions).

Scope: server only — DNS/DNSSEC left unguarded directly (cheap to recreate, and transitively covered at the tofu layer per above).

Verification

Ran locally on a freshly installed OpenTofu 1.12.0 (GPG-verified standalone install):

  • tofu fmt -check -recursive -diff — clean
  • tofu init -backend=false + tofu validate — root valid
  • ✅ per-module validate (bootstrap, cloudflare, hetzner) — all valid
  • ✅ confirmed via tofu providers schema -json that hcloud_server exposes delete_protection + rebuild_protection

validate passing on 1.12 confirms the dynamic prevent_destroy reference is accepted (it errors on older versions). The live destroy-plan behavior check needs real Hetzner/Cloudflare credentials + state and was not run here.

🤖 Generated with Claude Code

… VPS

OpenTofu 1.12 lets prevent_destroy reference input variables. Use it to
guard module.hetzner.hcloud_server.main — the single host carrying every
persistent Docker volume (Postgres, acme.json, GlitchTip) — against an
accidental destroy/replace, while keeping the deliberate-rebuild path open
by flipping the gate off.

- main.tf: required_version >= 1.6.0 -> >= 1.12.0; wire prevent_server_destroy
  through to the hetzner module.
- variables.tf: new prevent_server_destroy (bool, default true).
- modules/hetzner: new prevent_destroy var; add prevent_destroy = var.prevent_destroy
  to hcloud_server.main lifecycle (keeps ignore_changes = [user_data]); update
  rebuild comment.
- CI: pin opentofu/setup-opentofu to tofu_version 1.12.0.
- terraform.tfvars.example: document the toggle.
- .terraform.lock.hcl: full cross-platform h1: checksums added by 1.12 init.

To deliberately rebuild the host:
  tofu apply -var prevent_server_destroy=false \
    -replace=module.hetzner.hcloud_server.main

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the infra/bootstrap OpenTofu stack to require OpenTofu 1.12 and uses 1.12’s variable-aware lifecycle.prevent_destroy to protect the single stateful Hetzner VPS from accidental destroy/replace, while keeping an explicit “deliberate rebuild” path available via a toggle variable.

Changes:

  • Raise infra/bootstrap’s required_version floor to >= 1.12.0 and pin CI validation to OpenTofu 1.12.0.
  • Introduce prevent_server_destroy (default true) and wire it into the Hetzner module’s hcloud_server.main lifecycle.prevent_destroy.
  • Update example tfvars and lockfile checksums accordingly.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
infra/bootstrap/variables.tf Adds prevent_server_destroy toggle variable (default true).
infra/bootstrap/terraform.tfvars.example Documents how to disable the guard for a deliberate rebuild.
infra/bootstrap/modules/hetzner/variables.tf Adds module input prevent_destroy to receive the guard flag.
infra/bootstrap/modules/hetzner/main.tf Applies prevent_destroy = var.prevent_destroy on hcloud_server.main lifecycle and updates inline rebuild docs.
infra/bootstrap/main.tf Raises OpenTofu version floor and wires prevent_server_destroy into the hetzner module.
infra/bootstrap/.terraform.lock.hcl Adds additional cross-platform provider checksums produced by OpenTofu 1.12 init.
.github/workflows/infra-bootstrap-validate.yml Pins CI to OpenTofu 1.12.0 for fmt/validate.
Files not reviewed (1)
  • infra/bootstrap/.terraform.lock.hcl: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@agjs agjs changed the title feat(infra): OpenTofu 1.12 + var-gated prevent_destroy on the VPS feat(infra): OpenTofu 1.12 + defense-in-depth VPS protection Jun 11, 2026
… layer

Defense in depth: prevent_destroy only guards tofu runs through this
config + state. Add delete_protection + rebuild_protection on
hcloud_server.main — API-enforced locks that block deletion/rebuild from
the Hetzner console, hcloud CLI, raw API, or a tofu run with broken
state. Both flags are driven by the same prevent_server_destroy gate, so
there is one explicit switch: default on => tofu AND Hetzner both refuse;
flip false + apply => locks lift, then -replace/destroy works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@agjs
agjs merged commit d53f168 into main Jun 11, 2026
31 checks passed
@agjs
agjs deleted the infra/opentofu-1.12-prevent-destroy branch June 11, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants