feat(infra): OpenTofu 1.12 + defense-in-depth VPS protection - #166
Merged
Conversation
… 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>
There was a problem hiding this comment.
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’srequired_versionfloor to>= 1.12.0and pin CI validation to OpenTofu1.12.0. - Introduce
prevent_server_destroy(defaulttrue) and wire it into the Hetzner module’shcloud_server.mainlifecycle.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.
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 letsprevent_destroyreference 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, defaulttrue):lifecycle.prevent_destroyon the server. Rejects any plan that would destroy or-replaceit. Because a blankettofu destroyaborts entirely if any resource is protected, this also transitively shields the DNS records + DNSSEC at the tofu layer.delete_protection+rebuild_protectionon the server. Enforced by Hetzner's API regardless of tooling: blocks deletion/rebuild from the Hetzner console,hcloudCLI, 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)
The API lock must be lifted by an apply before a destroy can succeed, which is exactly the accident-resistance we want.
Other changes
main.tf:required_version>= 1.6.0→>= 1.12.0(dynamicprevent_destroyerrors on older versions).opentofu/setup-opentofutotofu_version: "1.12.0"(was unpinned/latest).terraform.tfvars.exampledocuments the single toggle; the server's inline comment documents both layers and the rebuild path..terraform.lock.hclgains the full set of cross-platformh1:checksums that 1.12's improved checksum handling adds oninit(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— cleantofu init -backend=false+tofu validate— root validtofu providers schema -jsonthathcloud_serverexposesdelete_protection+rebuild_protectionvalidatepassing on 1.12 confirms the dynamicprevent_destroyreference 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