You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(infra): align tofu provisioning docs with VPS protection + R2 state
The provisioning-with-tofu page lagged behind the infra changes:
- "Destroying" told users `tofu destroy` wipes the server — now WRONG, since
prevent_destroy + Hetzner delete_protection refuse it. Rewritten to document
the deliberate two-step gate-lowering (apply to lift locks, then destroy).
- "State management" showed a bare s3 block (bucket/key/region only). Replaced
with the pre-wired Cloudflare R2 flow this PR adds: backend.hcl partial
config, env-var credentials, use_lockfile, and init -migrate-state.
- Prerequisites now call out the OpenTofu 1.12+ requirement.
- terraform.tfvars shape documents prevent_server_destroy.
- Design choices: updated the state bullet and added a defense-in-depth bullet
covering both protection layers.
Verified: astro build succeeds, check:rendered-markdown passes, and
check:fragments confirms the new #destroying / #state-management /
#design-choices anchors resolve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: apps/docs/src/content/docs/topics/provisioning-with-tofu.mdx
+34-12Lines changed: 34 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,7 +17,7 @@ You'll need the following before starting:
17
17
-**Cloudflare API token**: Cloudflare, My Profile, API Tokens, Custom Token. Scope: `Zone:DNS:Edit`, `Zone:Zone Settings:Edit`, `Zone:Rulesets:Edit` on the target zone.
18
18
-**Cloudflare zone ID**: Zone overview page in the dashboard, right sidebar.
19
19
-**SSH key**: Run `ssh-keygen -t ed25519` if you don't have one. You'll paste the `.pub` contents.
20
-
-**OpenTofu binary**: `brew install opentofu` on macOS. See [install docs](https://opentofu.org/docs/intro/install/) for other OSes.
20
+
-**OpenTofu binary**: `brew install opentofu` on macOS — version **1.12 or newer** (the stack uses variable-driven `prevent_destroy` and native state locking). See [install docs](https://opentofu.org/docs/intro/install/) for other OSes.
21
21
22
22
## Apply
23
23
@@ -97,6 +97,10 @@ domain = "boringstack.example"
97
97
vps_type = "cx32" # 4 vCPU / 8 GB
98
98
vps_location = "fsn1"
99
99
100
+
# Protection (default true). Blocks tofu *and* Hetzner from destroying the
101
+
# VPS; set false only for a deliberate rebuild or teardown.
102
+
prevent_server_destroy = true
103
+
100
104
# Stack secrets
101
105
jwt_secret = "..." # 32+ chars
102
106
postgres_password = "..."
@@ -126,20 +130,31 @@ Everything in `terraform.tfvars.example` ships with comments explaining what it'
126
130
127
131
## State management
128
132
129
-
For a single operator: state file is local and gitignored (default config).
133
+
State defaults to a local `terraform.tfstate` — gitignored, and fine for a single operator spinning something up quickly. But local state is a single point of failure: lose the file and you can no longer plan, reconcile, or safely destroy the stack — more so now that the VPS is delete-protected (see [Destroying](#destroying)).
130
134
131
-
For a team: point the OpenTofu backend at S3 (or any S3-compatible store like Cloudflare R2, Backblaze B2, or Hetzner Object Storage). Add one block to `main.tf`:
135
+
For anything long-lived, move state to **Cloudflare R2** (S3-compatible, no egress fees, an account you already have for DNS). `main.tf` ships a ready-to-uncomment `backend "s3"` block pre-filled with the R2-specific flags (`use_path_style`, the `skip_*` preflight toggles, `skip_s3_checksum`) and `use_lockfile = true` for native locking — no DynamoDB table. Deployment-specific values live in a gitignored `backend.hcl`, copied from `backend.hcl.example`:
Credentials stay in the `AWS_*` env vars, never on disk. Hetzner Object Storage, Backblaze B2, or AWS S3 work the same way — only `endpoints.s3` (and the `skip_*`/`use_path_style` flags, for true AWS) differ.
157
+
143
158
The state file contains secrets (cloud-init renders with sensitive values). Encrypt at rest and restrict bucket access. Same security posture as everywhere else in BoringStack.
144
159
145
160
## Updating
@@ -176,11 +191,17 @@ The `bootstrap` module talks to cloud-init, which every major cloud accepts. Swa
176
191
177
192
## Destroying
178
193
194
+
The VPS is protected by two independent layers (see [Design choices](#design-choices)): OpenTofu's `prevent_destroy` and Hetzner's API-level `delete_protection`/`rebuild_protection`. Both are driven by the `prevent_server_destroy` variable, which defaults to `true`. So a plain `tofu destroy` is **refused** — by design, you can't nuke production in a single command.
195
+
196
+
To deliberately tear down, lower the gate first, then destroy:
197
+
179
198
```bash
180
-
tofu destroy
199
+
# Set prevent_server_destroy = false in terraform.tfvars (or pass -var on each command).
200
+
tofu apply # lifts the Hetzner delete/rebuild locks in place
201
+
tofu destroy # now succeeds
181
202
```
182
203
183
-
This wipes the Hetzner server, removes Cloudflare records, deletes the firewall and SSH key, and reverts Cloudflare zone settings to defaults. The state file remains. Run`rm terraform.tfstate*` for full cleanup.
204
+
The `apply` step is required: Hetzner won't honour a delete while the API lock is still set, so the lock must be lifted by an apply *before*`destroy` can remove the server. This wipes the Hetzner server, removes Cloudflare records, deletes the firewall and SSH key, and reverts Cloudflare zone settings to defaults. With remote state the state object stays in the bucket; with local state, run`rm terraform.tfstate*` for full cleanup.
184
205
185
206
## Troubleshooting
186
207
@@ -209,7 +230,8 @@ The runtime repos work fine without this one. It's a convenience layer, not a de
209
230
-**Sane Cloudflare zone defaults**: SSL strict, HSTS 6 months, TLS min 1.2, browser integrity on. Matches what `production-labels.yml` expects. Each setting is one override away.
210
231
-**DNS: apex and www only**: one A/AAAA pair on the apex serves both the SPA and `/api/*` via same-origin path routing. `www.` is a CNAME to apex with a redirect rule. No `api.` subdomain. Traefik path-routes `/api/*` on the same host.
211
232
-**Edge bot-blocking, on by default**: a single Cloudflare WAF custom rule (`http_request_firewall_custom`) blocks common scanner probes — `/.env`, `/.git/`, `/wp-admin`, `/xmlrpc`, … — at the edge, so they never reach Traefik or the API. Configurable via `bot_block_paths`; disable with `enable_bot_blocking = false`. Works on the Cloudflare Free plan.
212
-
-**State stays local by default**: single-operator default. An S3 backend block is one paste away for teams.
233
+
-**State stays local by default, R2 one step away**: single-operator default is a local state file. `main.tf` ships a pre-wired, commented Cloudflare R2 backend (S3-compatible, native locking, no DynamoDB); enabling it is `cp backend.hcl.example backend.hcl`, uncomment the block, and `tofu init -migrate-state`. See [State management](#state-management).
234
+
-**Production is hard to nuke, on purpose**: the VPS carries OpenTofu `prevent_destroy`*and* Hetzner API-level `delete_protection`/`rebuild_protection`, both gated behind a single `prevent_server_destroy` variable (default on). Defense in depth — destroying or replacing the box (and the Postgres, ACME, and GlitchTip volumes it holds) takes a deliberate, explicit gate-lowering, not a stray `tofu destroy` or a misclick in the Hetzner console. See [Destroying](#destroying).
213
235
-**Secrets in terraform.tfvars (gitignored)**: same pragmatic floor as `compose/.env`. Upgrade to a secret manager when team size demands it.
214
236
-**Outputs print, never side-effect**: apply prints the IP, ssh command, and site URL. Never auto-opens anything.
215
237
-**Optional bootstrap repo, separate from infra-compose**: same logic as the planned Kubernetes template. Separation lets operators skip the tool entirely.
0 commit comments