From 758544322762004e84df9cecc7de4599dfbe8e3f Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 22 Oct 2026 22:24:48 +0200 Subject: [PATCH] =?UTF-8?q?night=202026-10-23=20so=20far:=20D30=20live=20p?= =?UTF-8?q?roof=20(re-key=20=E2=86=92=20new-key=20handshake;=20ep0-started?= =?UTF-8?q?=20handshakes=20heal=20a=20short=20clock=20jump=20by=20themselv?= =?UTF-8?q?es),=20D4=20patches=20v2=20after=20review,=20test-night=20insta?= =?UTF-8?q?ll/fixture=20evidence,=20DooPlex=20survival=20survey=20(read=20?= =?UTF-8?q?only)=20+=20PLAN,=2003/05/07=20+=20CONTEXT,=20total-loss=20runb?= =?UTF-8?q?ook:=20MAIL-HOLD=20shipped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTEXT.md | 19 ++ documentation/architecture/03-host-agent.md | 15 +- .../architecture/05-hub-architecture.md | 7 + .../architecture/07-backup-architecture.md | 15 ++ .../dooplex-survival-2026-10-22/PLAN.md | 70 ++++++ .../readings/01-timers.txt | 7 + .../readings/02-journals.txt | 43 ++++ .../readings/03-offsite-content.txt | 31 +++ .../readings/04-gc-gitea.txt | 11 + .../readings/05-local.txt | 32 +++ .../readings/06-public-deps.txt | 8 + .../readings/07-k8s.txt | 63 +++++ .../P2-D30/01a-run1-clock-ahead.txt | 4 + .../P2-D30/02a-run1-clock-back.txt | 3 + .../P2-D30/03a-run1-ep0-initiated-heal.txt | 15 ++ .../P2-D30/04-run2-block-and-ahead.txt | 5 + .../P2-D30/05-run2-clock-back.txt | 3 + .../P2-D30/06-run2-after-rekey.txt | 10 + .../P2-D30/07-run2-new-key-handshake.txt | 9 + .../P2-D30/08-hub-db-read.txt | 11 + .../audits/night-2026-10-23/P2-D30/README.md | 18 ++ .../P3-review/felhom-agent-bundle-D4-v2.patch | 222 ++++++++++++++++++ .../P3-review/felhom.eu-installer-D4-v2.patch | 29 +++ .../night-2026-10-23/P4-testnight/01-bind.txt | 4 + .../P4-testnight/02-claim.txt | 3 + .../P4-testnight/03-agent-and-d4-fixture.txt | 17 ++ .../P4-testnight/04-drive-init.txt | 10 + .../P4-testnight/05-installs.txt | 17 ++ .../P4-testnight/06-loader-ct.txt | 7 + .../P4-testnight/install/00-vm-create.txt | 22 ++ .../P4-testnight/timeline.txt | 4 + .../runbooks/total-loss-of-dooplex.md | 2 +- 32 files changed, 734 insertions(+), 2 deletions(-) create mode 100644 documentation/audits/dooplex-survival-2026-10-22/PLAN.md create mode 100644 documentation/audits/dooplex-survival-2026-10-22/readings/01-timers.txt create mode 100644 documentation/audits/dooplex-survival-2026-10-22/readings/02-journals.txt create mode 100644 documentation/audits/dooplex-survival-2026-10-22/readings/03-offsite-content.txt create mode 100644 documentation/audits/dooplex-survival-2026-10-22/readings/04-gc-gitea.txt create mode 100644 documentation/audits/dooplex-survival-2026-10-22/readings/05-local.txt create mode 100644 documentation/audits/dooplex-survival-2026-10-22/readings/06-public-deps.txt create mode 100644 documentation/audits/dooplex-survival-2026-10-22/readings/07-k8s.txt create mode 100644 documentation/audits/night-2026-10-23/P2-D30/01a-run1-clock-ahead.txt create mode 100644 documentation/audits/night-2026-10-23/P2-D30/02a-run1-clock-back.txt create mode 100644 documentation/audits/night-2026-10-23/P2-D30/03a-run1-ep0-initiated-heal.txt create mode 100644 documentation/audits/night-2026-10-23/P2-D30/04-run2-block-and-ahead.txt create mode 100644 documentation/audits/night-2026-10-23/P2-D30/05-run2-clock-back.txt create mode 100644 documentation/audits/night-2026-10-23/P2-D30/06-run2-after-rekey.txt create mode 100644 documentation/audits/night-2026-10-23/P2-D30/07-run2-new-key-handshake.txt create mode 100644 documentation/audits/night-2026-10-23/P2-D30/08-hub-db-read.txt create mode 100644 documentation/audits/night-2026-10-23/P2-D30/README.md create mode 100644 documentation/audits/night-2026-10-23/P3-review/felhom-agent-bundle-D4-v2.patch create mode 100644 documentation/audits/night-2026-10-23/P3-review/felhom.eu-installer-D4-v2.patch create mode 100644 documentation/audits/night-2026-10-23/P4-testnight/01-bind.txt create mode 100644 documentation/audits/night-2026-10-23/P4-testnight/02-claim.txt create mode 100644 documentation/audits/night-2026-10-23/P4-testnight/03-agent-and-d4-fixture.txt create mode 100644 documentation/audits/night-2026-10-23/P4-testnight/04-drive-init.txt create mode 100644 documentation/audits/night-2026-10-23/P4-testnight/05-installs.txt create mode 100644 documentation/audits/night-2026-10-23/P4-testnight/06-loader-ct.txt create mode 100644 documentation/audits/night-2026-10-23/P4-testnight/install/00-vm-create.txt create mode 100644 documentation/audits/night-2026-10-23/P4-testnight/timeline.txt diff --git a/CONTEXT.md b/CONTEXT.md index 436d09bb..1f44b9eb 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -16,6 +16,25 @@ > and holds nothing of its own; this file does hold its own content, namely the standing rulings below. +> **2026-10-22 evening — operator rulings D29, D30, D31 (CC's picks accepted; the decide rows now in `STATUS-HISTORY.md`).** +> **D29** = no: the controller repository stays public for now (R-925). **D30** = the agent makes a new WireGuard key and +> registers it on the existing peer path (R-1204). **D31** = the hub skips a deleted install that never made a key when it +> decides the automatic PBS re-issue (R-1205). If the operator reverses one in the morning, its commit is reverted before +> release (night brief 2026-10-23 §0). + +> **2026-10-22/23 night — what was built (night brief; nothing released, deployed, published or vouched).** +> **Stray-copy fix** (R-1214/R-1180): controller `66a69d4` + `912e592` — the capped copy's container has a fixed name + +> label `felhom.tier2-copy=1`, is removed by name on any error, every leftover is removed at start before the scheduler +> and web server, a restore is refused while a copy runs (`backup.Tier2CopyBlocksRestore`, in-memory record + a stray +> flag until the start-up removal succeeds); `--mount`, image ID, `--pull never`, `--no-healthcheck`. Proven on 9202 rc.5. +> **D30:** agent `ae69ad4` (`wgtunnel/rekey.go`, 15 min, once an hour, `private.key.prev`); hub `84751cb0` pins the +> alarm clearing. **D31:** hub `84751cb0` `store.LatestKeyHoldingHostDeletion` (hub 0.167.0 source, NOT built — a hub +> image build is the morning's, with the deploy). **Review fixes:** controller `943c517` (C5 catch-up releases the +> whole-guest hold before its checks; C3 text; C6/C8 wording); the D4 patches v2 (`audits/night-2026-10-23/P3-review/`). +> **Images:** controller `0.340.0-rc.5` (= `66a69d4`) and `0.340.0-rc.6` (= `943c517`); no hub image tonight. +> **Survey:** `audits/dooplex-survival-2026-10-22/PLAN.md` (G1–G10; the three biggest: the sheet, the registry, the +> home line). Report: `REPORT-night-2026-10-23.md`. + > **2026-10-22 15:47 — operator rulings, „all picks" (the decide table, rows now in `STATUS-HISTORY.md`).** > **D3** = change the zero-knowledge wording now, look again after the first sale (R-985). **D4** = root SSH from the > LAN by key only; the console password stays (R-982). **D5** = approve the minimum machine, `audits/sizing-2026-10-13/SIZING.md` diff --git a/documentation/architecture/03-host-agent.md b/documentation/architecture/03-host-agent.md index 282ad639..320a68e5 100644 --- a/documentation/architecture/03-host-agent.md +++ b/documentation/architecture/03-host-agent.md @@ -131,7 +131,20 @@ the one trust root; the agent's API token is untouched; a box gets the new rule **[DESIGN, ruled 2026-10-22 15:47 (D4, R-982) — built as a ready patch, NOT shipped] Root SSH from the LAN by key only; the console password stays.** The config bundle gains one fixed file, `/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf` (`PermitRootLogin prohibit-password`), checked and reloaded by `felhom-os-apply`; the installer's uninstall removes it. Why a drop-in: Debian's `sshd_config` Includes `sshd_config.d/*` FIRST and sshd keeps the first value it reads, so the drop-in wins over the stock line; the out-of-band operator sshd (`FELHOM_SSHD`, port 8822) has its own config with no Include and is not touched. Proven on a fresh ISO 1.30.0 install: password SSH refused after, key SSH and the PVE web password login (200) and the console login still work (`audits/night-2026-10-22/P2-D4/`). It ships with the next agent release + installer tag. -**[FACT, agent main `a0c88e5`, unreleased] The heartbeat reports a WireGuard handshake from the future (R-1204).** WireGuard refuses an initiation whose TAI64N time is not newer than the last one accepted from that peer, so a box that once ran with its clock ahead is locked out of ep0 until real time passes that moment. The agent used to clamp the negative age to 0 (it looked healthy); `wgtunnel.handshakeFuture` now sets `handshake_future_s` when the handshake is more than 60 s ahead (`TestR1204_AFutureHandshakeIsReported`). The hub alarms on it (`05` §15.4b). The repair is STATUS decision D30. +**[FACT, agent main `a0c88e5`, unreleased] The heartbeat reports a WireGuard handshake from the future (R-1204).** WireGuard refuses an initiation whose TAI64N time is not newer than the last one accepted from that peer, so a box that once ran with its clock ahead is locked out of ep0 until real time passes that moment. The agent used to clamp the negative age to 0 (it looked healthy); `wgtunnel.handshakeFuture` now sets `handshake_future_s` when the handshake is more than 60 s ahead (`TestR1204_AFutureHandshakeIsReported`). The hub alarms on it (`05` §15.4b). + +**[DESIGN, operator ruling D30 2026-10-22; agent main `ae69ad4`, unreleased] The tunnel heals itself by a new key.** When +the latest handshake stays stamped in the future for 15 minutes, the agent makes a new WireGuard key pair (the old +private key kept as `wg/private.key.prev`, 0600 — the escrow may hold it) and registers the new public key on the +existing path, `POST /hosts//wg`: the hub re-keys the peer in place (same /32, generation bump) and its full-list +push to ep0 (`wgsync`, `wg syncconf`) drops the old key and adds the new one. A new key is a new peer for ep0, with no +stored timestamp. Once an hour at most; not while the unit is down; never during a re-register already in flight. The +interface switches at the agent's next desired-state fetch (up to its report interval). **`[FACT]` 2026-10-23 night +(test box Tester4-16fdef, `audits/night-2026-10-23/P2-D30/`):** with ep0 free to start a handshake of its own, a 2-hour +clock jump healed by itself in ~3 minutes (ep0 had traffic for the box; the box accepts ep0's timestamps) and the agent +correctly did not re-key; with ep0's handshake starts dropped on the box (the rehearsal's condition), the tunnel stayed +dead and the agent re-keyed after 15 minutes. **Known gap:** a reboot after the clock was corrected forgets the +future stamp (R-1223). ## 4. Control model — reconcile + signed destructive ops diff --git a/documentation/architecture/05-hub-architecture.md b/documentation/architecture/05-hub-architecture.md index ce1c8860..6f4e7223 100644 --- a/documentation/architecture/05-hub-architecture.md +++ b/documentation/architecture/05-hub-architecture.md @@ -371,6 +371,7 @@ token id, fingerprint, datastore, tunnel IP, `secret_generation`) with its consu | **re-issue, descriptor ABSENT, DR flag on** (R-511, v0.114.0) | **adopted**: re-keyed | **rebuilt** from the endpoint's answer; `pbsdr_adopted` audit row | | host delete | **kept** (tenancy survives) | goes with the host | | host delete acknowledged through escrow-ack, then a new box | re-keyed automatically (F-14) | rebuilt | +| …and a later install deleted before it made a key, then a new box (D31, operator 2026-10-22; hub 0.167.0 source) | re-keyed automatically — the gate reads the newest deletion of a host that HELD A KEY (`LatestKeyHoldingHostDeletion`) | rebuilt | | RESET / customer delete | **deprovisioned** — namespace, every backup group AND token | purged | **Why adopt exists:** a rebuilt box's WG hook refused („the endpoint already holds a PBS token … use @@ -459,6 +460,12 @@ date taken) are shown on its own app page, on its box, until it dismisses them **[FACT, hub `e267c5e2`]** The host report carries the agent's `wireguard` block (`hostWireguard`). `checkWGTunnel` (`api/host_tunnel.go`, called after `checkHostSwap`) calls the tunnel stale when the box reports `handshake_future_s` > 0 (the clock-ahead lock-out) or a last handshake older than 6 h (healthy boxes measured at most 113–124 s over 24 h). It is edge-triggered against the host's latest tunnel event (`store.LatestHostTunnelEvent`): `wg_tunnel_stale` once on the way down, `wg_tunnel_ok` once on the way back; both are allow-listed and operator-only (`notify.operatorOnlyEvents`, `wg_tunnel_operator_only_test.go`) — the household cannot repair a tunnel. A box with no `wireguard` block (an agent before the field, or no tunnel) raises nothing. +**[FACT, hub `84751cb0`, 0.167.0 source] The repair clears it (D30).** An agent with D30 (`03` §R-1204) registers a new key +after 15 minutes of a future-stamped handshake; the new key's first handshake makes the report fresh, and the alarm +records `wg_tunnel_ok` once (`TestD30_TheAlarmClearsWhenTheNewKeyShakesHands`). `wg_tunnel_ok` is an info event: +stored, not mailed. **Known gaps (rows):** a reboot after the clock was corrected sends no handshake at all and is not +judged (R-1223); the operator's mail cooldown is per customer, not per host (R-1224). + ### 15.5 How "the Hungarian did not change" is known 56 goldens captured from v0.117.0 before any string moved, in diff --git a/documentation/architecture/07-backup-architecture.md b/documentation/architecture/07-backup-architecture.md index a6ddc05e..526d6559 100644 --- a/documentation/architecture/07-backup-architecture.md +++ b/documentation/architecture/07-backup-architecture.md @@ -1590,6 +1590,21 @@ or pattern. **Why:** the data-loss hunt of 2026-10-20 proved two nights that del (R-1175: one share group's reconcile removed the other groups' copies, and a dead drive's copy; R-1181: a missing or emptied folder took its copy within one night). Controller: `internal/backup/tier2_kept.go`. +### 8.2b The second-drive copy runs in a memory-capped container that never outlives the copy [DESIGN + FACT, R-1214, 2026-10-22/23 nights; rides controller 0.340.0] + +The copy (`rsync -a --delete`) runs in a container of the controller's own image, capped at 512 MB, so its page cache +is reclaimed inside the cap instead of from the apps (R-1214: full stall 5.7 s → 0.37 s on 9202). Only for paths the +controller and the Docker daemon see alike (`/mnt/…`, no symlink); the daemon must see the path itself (`--mount` +refuses a missing one — then the in-process copy runs). **The container must never outlive the copy** (the stray-copy +fix, 2026-10-23 night): a fixed name and the label `felhom.tier2-copy=1`; on any error, the timeout included, it is +removed by name; at controller start, before the scheduler and the web server, every labelled container is removed; +and a restore is refused while any second-drive copy runs or a leftover may still run (the other direction of R-1180 — +a `--delete` beside a restore removes what the restore brings back). **`[FACT]` 9202, rc.5 +(`audits/night-2026-10-23/P1-straycopy/`):** a controller restart in the middle of a 16 GB copy → the new controller +removed the old container at start; a restore probe during the copy was refused; the redone copy equals the source; a +15-second test limit → the container removed, the partial copy stopped growing. Controller: +`internal/backup/tier2_copy_lifecycle.go`. + ### 8.1 The blank cells, listed explicitly Per the rule that a blank is a finding, here they are: diff --git a/documentation/audits/dooplex-survival-2026-10-22/PLAN.md b/documentation/audits/dooplex-survival-2026-10-22/PLAN.md new file mode 100644 index 00000000..eee3b24a --- /dev/null +++ b/documentation/audits/dooplex-survival-2026-10-22/PLAN.md @@ -0,0 +1,70 @@ +# DooPlex survival survey — night 2026-10-22 → 23 (read only) + +Night brief Part 5. **Nothing on DooPlex was changed.** Every number below was read on DooPlex tonight (unit state, +journal lines, `du`, `kubectl get`, DNS, HTTP status) — raw readings in `readings/`. Secrets are named, never read: +no key file, token file or password was opened. ep0 was not contacted (the nightly jobs' own journal lines are the +evidence of what reached it). Read with `runbooks/total-loss-of-dooplex.md` (the order of steps) and the 2026-10-09 +plan (`audits/dooplex-survival-2026-10-09/PLAN.md`, which built the nightly copy). + +## 1. What lives on DooPlex, and what saves it + +| Thing | Where it lives | What backs it up | Where the copy is | Last copy (measured) | Who can restore | How long | +|---|---|---|---|---|---|---| +| **Gitea** — 12 repositories (845 MB), DB in CNPG, `app.ini` | PVC `gitea-data` (Longhorn, `sdb1`) | (1) 6-hourly `pg_dump` + Longhorn backup, both on `sda1` (same box); (2) `felhom-dooplex-offsite` nightly 00:20 | ep0, namespace `operator`, `host/dooplex-gitea`, encrypted (key **S1**) | 2026-10-22 00:22, 736 MiB (425 MiB new), 42 s, success | the operator with S1 + the read token S7 (or ep0 root) | restore ~1 min + start ~1 min on a ready machine (walked 2026-10-09 Part C); on new hardware add k3s/Docker install — hours | +| **Weekly proof of that copy** | DooPlex timer, Sunday 05:30 | — | — | 2026-10-18: 27 739 files match, **11** repositories pass `git fsck`, `gitea.dump` readable, Vaultwarden 1 user / 810 items | — | — | +| **Container registry + generic packages** (controller/hub images, agent packages) | `/data/gitea/packages` in `gitea-data`, **38.6 GB** | Longhorn backup on `sda1` only | **nowhere off DooPlex** (left out on purpose 2026-10-09: „rebuilt from the code") | — | rebuild from the restored code on a build machine | never timed | +| **Hub + its DB** (`hub.db` ~493 MiB) | PVC `hub-data` (Longhorn) | Longhorn backup (same box); `felhom-hub-db-backup` nightly 02:30 | ep0 `operator`, `host/dooplex-hub`, encrypted (key **S2**); seal key **S4** needed to open its sealed passwords | 2026-10-22 02:31, 493 MiB (24 MiB compressed), 6 s, success | the operator with S2 + S4 + S7 | ~5 min on a bench with k3s ready (2026-10-09 Part D) | +| **Weekly proof of that copy** | Sunday 04:31 | — | — | 2026-10-18: integrity ok, 4 hosts, 4 sealed console passwords, 0 readable | — | — | +| **Operator signing keys** (3 files, 0600, no passphrase) | `/mnt/5_hdd/felhom.eu/` on `sda1` (the same disk as the local backup tree) | inside the nightly `host/dooplex-gitea` copy (`signing/`; a missing key refuses the push) | ep0, behind S1; paper (S6) **if printed** | with the Gitea copy (2026-10-22) | the operator with S1, or the printout | minutes | +| **Password manager** (Vaultwarden, 1 user, 810 items) | PVC `vaultwarden-data` (Longhorn) | Longhorn backup (same box); nightly copy (its own `vaultwarden backup` + `rsa_key.pem`) | ep0, behind S1; the vault itself opens with the master password S3 | with the Gitea copy (2026-10-22) | the operator with S1 + S3 | started once on a throwaway (2026-10-09) | +| **k8s Secrets** (every Secret, GPG-encrypted with S5) | nightly export `/mnt/5_hdd/backup/secrets` (136 MB, 30 days) | the same file rides the nightly copy | ep0 behind S1, then S5 | 2026-10-22 | the operator with S1 + S5 | minutes | +| **CI** (`act-runner` pod, PVC `act-runner-data`) | k3s | nothing needed: re-registered from Gitea | — | — | the operator | ~15 min | +| **Website, installer script, `hub.felhom.eu`, `gitea.dooplex.hu`** | DooPlex, behind the home line (all four resolve to the home address; measured) | the website and installer are code in the Gitea copy | ep0 (as code) | — | the operator + a DNS change at Cloudflare (S11) | after Gitea + k3s | +| **The ISO** | Cloudflare R2 (`iso.felhom.eu`) | — | not on DooPlex | — | — | survives | +| **ArgoCD** | k3s; Applications from `homelab-manifests` (in the Gitea copy) | its Secrets in the export | ep0 behind S1 + S5 | — | the operator | ~30 min; **the per-app auto-sync settings are set on the live objects, not in git** (several apps differ) | +| **DooPlex's own local backups** (R-232) | `/mnt/5_hdd/backup` on `sda1`: Longhorn 117 GB, `data` 53 GB, PostgreSQL dumps 2.1 GB, `ep0-copy` 21 GB, … | `dooplex-backup` nightly 03:00 | **the same box** | 2026-10-22 03:16, „All backup components completed successfully" | — | — | + +**Restore tests:** both weekly tests passed on 2026-10-18 (their next run is Sunday 2026-10-25). Since then the pod +lists **12** repositories against the test's 11; the nightly push refuses a copy with fewer repositories than the pod +lists, and it succeeded, so the twelfth is in the copy — the next Sunday test will show it. + +## 2. `total-loss-of-dooplex.md`, step by step — works today / stale / never tried + +| Step | Verdict | Why | +|---|---|---| +| 1. A machine and the sheet | **depends** | the sheet's printing is the operator's (R-923, R-924 „LEFT"); CC cannot see whether it exists | +| 2. Reach ep0's PBS | works **from DooPlex** (the nightly jobs use the tunnel); **never tried** from new hardware (WireGuard peer, Hetzner console) | | +| 3. A read token (S7) | works today | the weekly restore tests use it | +| 4. Restore `host/dooplex-gitea` with S1 | works today | the weekly test restores and checks it (2026-10-18) | +| 5. Vaultwarden first | works (once) | started on a throwaway 2026-10-09; its rows are checked weekly | +| 6. The Secrets export with S5 | works (once) | opened 2026-10-09; **the form of S5's offline copy is unknown to CC** | +| 7. Gitea, then rebuild the images | Gitea: works (once, 2026-10-09). Image rebuild: **never tried** | the registry is not copied | +| 8. The hub with S2 + S4, mail held | works (once, 2026-10-09 Part D) | **stale text:** it says the mail hold comes „from the next hub release" — it shipped in hub 0.145.0 (2026-10-10); fixed tonight | +| 9. DNS at Cloudflare | **never tried** | | +| 10. Signing | keys readable (the weekly test derives each public half) — **signing from the restored keys never tried** | | +| 11. Backups again | **never tried** on new hardware | `install.sh` exists in the code | + +## 3. The gaps — two options each, with CC's pick + +| # | Gap | Option A (cost: money / operator hours / risk) | Option B (cost) | CC's pick | +|---|---|---|---|---| +| G1 | **The paper sheet** — S1 opens every off-site copy (Gitea, Vaultwarden, signing keys, Secrets); without it no copy opens (R-923, R-924) | print `break-glass-sheet.md` filled (S1, S2, S4, S5, S6, S7, S8, S11, ep0 fingerprint) and store it outside the home, sealed — 0 € / ~30 min / paper can be lost or read | the key files on an encrypted USB stick kept outside the home + its passphrase in the head — ~10 € / ~1 h / one more secret to remember, a stick can die | **A** — it is the written plan; nothing else breaks the circle | +| G2 | **Signing keys without a passphrase** (R-924) | keep as the custody ruling (2026-09-16) says until the first sale; they ride the S1-encrypted copy — 0 € / 0 h / a DooPlex break-in can sign | a passphrase on the operational key and the recovery key off DooPlex — 0 € / ~2 h / every signed job then needs the operator; no unattended signing | **A** now; revisit at the first sale (the ruling stands) | +| G3 | **The password manager holds keys to DooPlex's own copies** (R-923) | the sheet (G1) breaks the circle — no extra cost | a monthly encrypted export to the operator's laptop — 0 € / 10 min a month / easy to forget | **A** (same act as G1) | +| G4 | **DooPlex's backups sit inside the same box** (R-232) — the Felhom parts are off-site now; the homelab apps, Authentik, Prometheus are not | accept for the homelab (not Felhom) — 0 € / 0 h / a homelab loss is total | restic of the chosen `/mnt/5_hdd/backup` sets to a Storage Box sub-account — ~4 €/month / ~2 h / ~170 GB first upload | **A** for Felhom's scope; the homelab is the operator's call | +| G5 | **Hub DB copy is up to 24 h old** — a box that enrolled or re-escrowed after 02:30 has no escrow row in a restored hub | keep nightly — 0 € / 0 h / up to a day of hub changes lost | push every 6 h (same job, same token, same prune) — 0 € / ~30 min (a DooPlex change, operator's yes) / ~4 × 24 MB a day on ep0 | **B** | +| G6 | **Gitea repositories off-site** — covered nightly, tested weekly | keep — 0 € | a second off-site mirror (Codeberg/GitHub private) — 0 € / ~1 h / code at a third party | **A** | +| G7 | **The registry and the agent packages exist only on DooPlex** — every box update, every reinstall and every new install pull from them | copy only what a box can be offered (the released controller/hub tags, the agent packages, the infra images) nightly to ep0 `operator` — 0 € / ~2 h CC + the operator's yes for a DooPlex change / a few GB on ep0 | a timed rebuild-from-code drill, then accept the outage — 0 € / ~3 h once / a rebuilt image is not the same digest (the „a version is pushed once" rule) | **A** | +| G8 | **Website, installer script and the hub all on the home line** — a DooPlex loss stops new installs and the public site | accept: Gitea and the hub come back first, then the site — 0 € / hours on the day / no installs meanwhile | serve the static site and the installer tag from Cloudflare (as the ISO already is) — 0 € / ~2 h / a second publish path to keep true | **B**, after the release week | +| G9 | **ArgoCD's per-app sync settings live only on the cluster** | write them into `homelab-manifests` — 0 € / ~30 min / none | re-set by hand after a loss from the memory notes — 0 € / ~1 h on the day | **A** | +| G10 | **S5's offline copy: form unknown to CC** | put S5 on the sheet (G1) — 0 € | re-encrypt the Secrets export with an S1-derived key — 0 € / ~1 h / a DooPlex change | **A** | + +**The three biggest:** G1 (no sheet → nothing opens), G7 (no registry copy → no update or install after a loss), +G8 (the website, installer and hub on one home line → new installs stop for hours or days). + +## 4. What tomorrow's hour decides (also in `STATUS.md`) + +1. G1 — print the sheet now? (CC: yes) +2. G7 — copy the release images and agent packages to ep0 nightly? (CC: yes) +3. G5 — hub DB every 6 hours? (CC: yes) +4. G8 — the site and installer on Cloudflare after the release week? (CC: yes, later) diff --git a/documentation/audits/dooplex-survival-2026-10-22/readings/01-timers.txt b/documentation/audits/dooplex-survival-2026-10-22/readings/01-timers.txt new file mode 100644 index 00000000..dc541be5 --- /dev/null +++ b/documentation/audits/dooplex-survival-2026-10-22/readings/01-timers.txt @@ -0,0 +1,7 @@ +=== felhom-dooplex-offsite: success Thu 2026-10-22 00:22:53 CEST 0 +=== felhom-hub-db-backup: success Thu 2026-10-22 02:31:31 CEST 0 +=== dooplex-backup: success Thu 2026-10-22 03:16:26 CEST 0 +=== felhom-hub-db-restore-test: success Sun 2026-10-18 04:31:19 CEST 0 +=== felhom-dooplex-offsite-restore-test: success Sun 2026-10-18 05:33:45 CEST 0 +=== backup-freshness: success Thu 2026-10-22 21:30:01 CEST 0 +=== felhom-ep0-copy-gc: success Thu 2026-10-22 08:00:00 CEST 0 diff --git a/documentation/audits/dooplex-survival-2026-10-22/readings/02-journals.txt b/documentation/audits/dooplex-survival-2026-10-22/readings/02-journals.txt new file mode 100644 index 00000000..2724f794 --- /dev/null +++ b/documentation/audits/dooplex-survival-2026-10-22/readings/02-journals.txt @@ -0,0 +1,43 @@ +=== felhom-dooplex-offsite +2026-10-22T00:22:09+02:00 dooplex felhom-dooplex-offsite[1390185]: Downloading previous manifest (Wed Oct 21 00:21:51 2026) +2026-10-22T00:22:09+02:00 dooplex felhom-dooplex-offsite[1390185]: Upload directory '/var/lib/felhom-dooplex-offsite/stage/root' to 'dooplex-hub@pbs!push@127.0.0.1:18007:felhom-offsite' as dooplex.pxar.didx +2026-10-22T00:22:49+02:00 dooplex felhom-dooplex-offsite[1390185]: dooplex.pxar: had to backup 424.942 MiB of 736.086 MiB (compressed 415.906 MiB) in 40.05 s (average 10.611 MiB/s) +2026-10-22T00:22:49+02:00 dooplex felhom-dooplex-offsite[1390185]: dooplex.pxar: backup was done incrementally, reused 311.144 MiB (42.3%) +2026-10-22T00:22:49+02:00 dooplex felhom-dooplex-offsite[1390185]: Uploaded backup catalog (1.402 MiB) +2026-10-22T00:22:51+02:00 dooplex felhom-dooplex-offsite[1390185]: Duration: 42.46s +2026-10-22T00:22:51+02:00 dooplex felhom-dooplex-offsite[1390185]: End Time: Thu Oct 22 00:22:51 2026 +2026-10-22T00:22:51+02:00 dooplex felhom-dooplex-offsite[1375323]: felhom-dooplex-offsite: pushed to ep0 (ns operator, host/dooplex-gitea) in 42 s +2026-10-22T00:22:51+02:00 dooplex felhom-dooplex-offsite[1375323]: felhom-dooplex-offsite: success signal written +2026-10-22T00:22:53+02:00 dooplex systemd[1]: felhom-dooplex-offsite.service: Deactivated successfully. +2026-10-22T00:22:53+02:00 dooplex systemd[1]: Finished felhom-dooplex-offsite.service - Felhom: push Gitea + DooPlex secrets to ep0, encrypted (R-232). +2026-10-22T00:22:53+02:00 dooplex systemd[1]: felhom-dooplex-offsite.service: Consumed 36.202s CPU time, 1.6G memory peak. +=== felhom-hub-db-backup +2026-10-22T02:31:18+02:00 dooplex felhom-hub-db-backup[2573155]: Encryption key fingerprint: b2:19:bf:36:3b:97:3d:6c +2026-10-22T02:31:18+02:00 dooplex felhom-hub-db-backup[2573155]: Downloading previous manifest (Wed Oct 21 02:30:55 2026) +2026-10-22T02:31:18+02:00 dooplex felhom-hub-db-backup[2573155]: Upload directory '/var/lib/felhom-hub-backup/stage' to 'dooplex-hub@pbs!push@127.0.0.1:18007:felhom-offsite' as hubdb.pxar.didx +2026-10-22T02:31:24+02:00 dooplex felhom-hub-db-backup[2573155]: hubdb.pxar: had to backup 492.879 MiB of 492.879 MiB (compressed 23.873 MiB) in 5.17 s (average 95.306 MiB/s) +2026-10-22T02:31:24+02:00 dooplex felhom-hub-db-backup[2573155]: Uploaded backup catalog (56 B) +2026-10-22T02:31:24+02:00 dooplex felhom-hub-db-backup[2573155]: Duration: 5.76s +2026-10-22T02:31:24+02:00 dooplex felhom-hub-db-backup[2573155]: End Time: Thu Oct 22 02:31:24 2026 +2026-10-22T02:31:24+02:00 dooplex felhom-hub-db-backup[2570694]: felhom-hub-db-backup: pushed hub-20261022T000000Z.db to ep0 (ns operator) in 6 s +2026-10-22T02:31:24+02:00 dooplex felhom-hub-db-backup[2570694]: felhom-hub-db-backup: success signal written +2026-10-22T02:31:31+02:00 dooplex systemd[1]: felhom-hub-db-backup.service: Deactivated successfully. +2026-10-22T02:31:31+02:00 dooplex systemd[1]: Finished felhom-hub-db-backup.service - Felhom: push the hub DB snapshot to ep0 (R-173). +2026-10-22T02:31:31+02:00 dooplex systemd[1]: felhom-hub-db-backup.service: Consumed 16.683s CPU time, 766M memory peak. +=== dooplex-backup +2026-10-22T03:16:26+02:00 dooplex backup-all.sh[2932973]: 115G /mnt/5_hdd/backup/longhorn-pvc +2026-10-22T03:16:26+02:00 dooplex backup-all.sh[2932973]: 15G /mnt/5_hdd/backup/minecraft-tlauncher +2026-10-22T03:16:26+02:00 dooplex backup-all.sh[2932973]: 35M /mnt/5_hdd/backup/paperless +2026-10-22T03:16:26+02:00 dooplex backup-all.sh[2932973]: 2.1G /mnt/5_hdd/backup/postgresql +2026-10-22T03:16:26+02:00 dooplex backup-all.sh[2932973]: 992K /mnt/5_hdd/backup/romm +2026-10-22T03:16:26+02:00 dooplex backup-all.sh[2932973]: 136M /mnt/5_hdd/backup/secrets +2026-10-22T03:16:26+02:00 dooplex backup-all.sh[2932973]: 4.8M /mnt/5_hdd/backup/sparkyfitness +2026-10-22T03:16:26+02:00 dooplex backup-all.sh[2970506]: [2026-10-22 03:16:26] [INFO] All backup components completed successfully! +2026-10-22T03:16:26+02:00 dooplex backup-all.sh[2970509]: [2026-10-22 03:16:26] [INFO] ======================================================== +2026-10-22T03:16:26+02:00 dooplex systemd[1]: dooplex-backup.service: Deactivated successfully. +2026-10-22T03:16:26+02:00 dooplex systemd[1]: Finished dooplex-backup.service - Dooplex Cluster Backup. +2026-10-22T03:16:26+02:00 dooplex systemd[1]: dooplex-backup.service: Consumed 5min 2.211s CPU time, 1.9G memory peak. +=== felhom-hub-db-restore-test +-- No entries -- +=== felhom-dooplex-offsite-restore-test +-- No entries -- diff --git a/documentation/audits/dooplex-survival-2026-10-22/readings/03-offsite-content.txt b/documentation/audits/dooplex-survival-2026-10-22/readings/03-offsite-content.txt new file mode 100644 index 00000000..283ebe98 --- /dev/null +++ b/documentation/audits/dooplex-survival-2026-10-22/readings/03-offsite-content.txt @@ -0,0 +1,31 @@ +=== felhom-hub-db-restore-test (last run) +2026-10-18T04:31:08+02:00 dooplex felhom-hub-db-restore-test[3899842]: Using encryption key from '/etc/felhom-hub-backup/enc.key'.. +2026-10-18T04:31:08+02:00 dooplex felhom-hub-db-restore-test[3899842]: Fingerprint: b2:19:bf:36:3b:97:3d:6c +2026-10-18T04:31:13+02:00 dooplex felhom-hub-db-restore-test[3899842]: restore complete (458.028 MiB processed in 4.9s, average 93.178 MiB/s) +2026-10-18T04:31:13+02:00 dooplex felhom-hub-db-restore-test[3899712]: felhom-hub-db-restore-test: checked: integrity ok, 4 host(s), 4 sealed console password(s), 0 readable +2026-10-18T04:31:13+02:00 dooplex felhom-hub-db-restore-test[3899712]: felhom-hub-db-restore-test: success signal written +2026-10-18T04:31:19+02:00 dooplex systemd[1]: felhom-hub-db-restore-test.service: Deactivated successfully. +2026-10-18T04:31:19+02:00 dooplex systemd[1]: Finished felhom-hub-db-restore-test.service - Felhom: restore-test the hub DB copy on ep0 (R-173). +2026-10-18T04:31:19+02:00 dooplex systemd[1]: felhom-hub-db-restore-test.service: Consumed 5.907s CPU time, 821.2M memory peak. +=== felhom-dooplex-offsite-restore-test (last run) +2026-10-18T05:32:13+02:00 dooplex felhom-dooplex-offsite-restore-test[269573]: progress 46% (291.015 MiB of 625.417 MiB in 36.5s, 5.911 MiB/s) +2026-10-18T05:32:53+02:00 dooplex felhom-dooplex-offsite-restore-test[269573]: progress 77% (485.113 MiB of 625.417 MiB in 1m 17.3s, 4.763 MiB/s) +2026-10-18T05:33:21+02:00 dooplex felhom-dooplex-offsite-restore-test[269573]: restore complete (625.417 MiB processed in 1m 45.4s, average 5.935 MiB/s) +2026-10-18T05:33:44+02:00 dooplex felhom-dooplex-offsite-restore-test[269413]: felhom-dooplex-offsite-restore-test: checked: 27739 files match the manifest, 11 repositories pass git fsck, gitea.dump readable, 3 secrets file(s), Vaultwarden 1 user(s) / 810 item +2026-10-18T05:33:44+02:00 dooplex felhom-dooplex-offsite-restore-test[269413]: felhom-dooplex-offsite-restore-test: success signal written +2026-10-18T05:33:45+02:00 dooplex systemd[1]: felhom-dooplex-offsite-restore-test.service: Deactivated successfully. +2026-10-18T05:33:45+02:00 dooplex systemd[1]: Finished felhom-dooplex-offsite-restore-test.service - Felhom: restore-test the Gitea + secrets copy on ep0 (R-232). +2026-10-18T05:33:45+02:00 dooplex systemd[1]: felhom-dooplex-offsite-restore-test.service: Consumed 35.494s CPU time, 931.3M memory peak. +=== offsite stage (names + sizes, no contents) +du: cannot access '/var/lib/felhom-dooplex-offsite/stage/root/*': No such file or directory +ls: cannot access '/var/lib/felhom-dooplex-offsite/stage/root/signing': No such file or directory +felhom-backup-failmail +felhom-backup-failmail@.service +felhom-dooplex-offsite +felhom-dooplex-offsite-restore-test +felhom-dooplex-offsite-restore-test.service +felhom-dooplex-offsite-restore-test.timer +felhom-dooplex-offsite.service +felhom-dooplex-offsite.timer +install.sh +test_dooplex_offsite.py diff --git a/documentation/audits/dooplex-survival-2026-10-22/readings/04-gc-gitea.txt b/documentation/audits/dooplex-survival-2026-10-22/readings/04-gc-gitea.txt new file mode 100644 index 00000000..970778c8 --- /dev/null +++ b/documentation/audits/dooplex-survival-2026-10-22/readings/04-gc-gitea.txt @@ -0,0 +1,11 @@ +=== felhom-ep0-copy-gc last run +2026-10-22T08:00:00+02:00 dooplex felhom-ep0-copy-gc[1347525]: ep0-copy-gc: done: 6 copy namespace(s), 6 on ep0, 0 absent, 0 to delete, 0 failed +2026-10-22T08:00:00+02:00 dooplex systemd[1]: Finished felhom-ep0-copy-gc.service - Felhom: remove a deleted customer's namespace from ep0-copy (R-901, decision 181). +=== gitea repos (pod listing) +pod gitea-system/gitea-5dbcbf7c7b-4j8qp +12 +845.4M /data/git/repositories +38.6G /data/gitea/packages +=== backup-freshness unit +Description=Backup freshness textfile metrics for localhost +ExecStart=/usr/local/sbin/backup-freshness.sh diff --git a/documentation/audits/dooplex-survival-2026-10-22/readings/05-local.txt b/documentation/audits/dooplex-survival-2026-10-22/readings/05-local.txt new file mode 100644 index 00000000..921e5622 --- /dev/null +++ b/documentation/audits/dooplex-survival-2026-10-22/readings/05-local.txt @@ -0,0 +1,32 @@ +=== /mnt/5_hdd/backup (names, sizes) +53G /mnt/5_hdd/backup/data +25G /mnt/5_hdd/backup/demo-felhom +21G /mnt/5_hdd/backup/ep0-copy +69M /mnt/5_hdd/backup/homelab-manifests +41M /mnt/5_hdd/backup/jarrs.eu +178M /mnt/5_hdd/backup/k3s +2.5M /mnt/5_hdd/backup/logs +117G /mnt/5_hdd/backup/longhorn-pvc +15G /mnt/5_hdd/backup/minecraft-tlauncher +35M /mnt/5_hdd/backup/paperless +2.1G /mnt/5_hdd/backup/postgresql +992K /mnt/5_hdd/backup/romm +136M /mnt/5_hdd/backup/secrets +4.8M /mnt/5_hdd/backup/sparkyfitness +=== longhorn-pvc sets (names) +backupstore +=== signing key files (names, modes, no contents) +-rw------- kisfenyo /mnt/5_hdd/felhom.eu/felhom-op-operational +-rw-rw-r-- kisfenyo /mnt/5_hdd/felhom.eu/felhom-op-operational.pub +-rw------- kisfenyo /mnt/5_hdd/felhom.eu/felhom-rec-recovery +-rw-rw-r-- kisfenyo /mnt/5_hdd/felhom.eu/felhom-rec-recovery.pub +-rw------- kisfenyo /mnt/5_hdd/felhom.eu/felhom_op_ed25519 +-rw-rw-r-- kisfenyo /mnt/5_hdd/felhom.eu/felhom_op_ed25519.pub +=== same disk? backup tree vs sources +Filesystem Mounted on +/dev/sda1 /mnt/5_hdd +/dev/sda1 /mnt/5_hdd +/dev/sdb1 / +/dev/sdb1 / + +[exited with code 0] diff --git a/documentation/audits/dooplex-survival-2026-10-22/readings/06-public-deps.txt b/documentation/audits/dooplex-survival-2026-10-22/readings/06-public-deps.txt new file mode 100644 index 00000000..6c3bad01 --- /dev/null +++ b/documentation/audits/dooplex-survival-2026-10-22/readings/06-public-deps.txt @@ -0,0 +1,8 @@ +felhom.eu -> 37.191.56.193 | 200 server= +www.felhom.eu -> 37.191.56.193 | 200 server= +hub.felhom.eu -> 37.191.56.193 | 302 server= +iso.felhom.eu -> 2a06:98c1:3121::1 | 404 server=cloudflare +gitea.dooplex.hu -> 37.191.56.193 | 200 server= +=== where the installer script is fetched from (installer docs / website) +https://iso.felhom.eu/felhom-installer-1.30.0-pve9.2-1.iso +https://iso.felhom.eu/felhom-installer-1.30.0-pve9.2-1.iso.sha256 diff --git a/documentation/audits/dooplex-survival-2026-10-22/readings/07-k8s.txt b/documentation/audits/dooplex-survival-2026-10-22/readings/07-k8s.txt new file mode 100644 index 00000000..4b03c114 --- /dev/null +++ b/documentation/audits/dooplex-survival-2026-10-22/readings/07-k8s.txt @@ -0,0 +1,63 @@ +=== pods: runner / argocd / vaultwarden / hub / website / registry +actualbudget-system/actualbudget-7fb44cd948-2xqjc Running +argocd/argocd-application-controller-0 Running +argocd/argocd-applicationset-controller-746fdcd449-s87sk Running +argocd/argocd-server-b7bb9755c-s96pt Running +felhom-system/contact-mailer-5bb869b85b-pqtjt Running +felhom-system/felhom-webpage-7f5c7968b5-g62t6 Running +felhom-system/filebrowser-59ff87cd88-c8qqm Running +felhom-system/hub-676bcc45cc-gnbzp Running +felhom-system/umami-d84d978f8-95rsv Running +felhom-system/umami-db-5fd98f59c5-slhcw Running +gitea-system/act-runner-84ff4656c4-t2857 Running +gitea-system/gitea-5dbcbf7c7b-4j8qp Running +vaultwarden-system/vaultwarden-7d967778cf-5mkr5 Running +=== PVCs of those namespaces (name, size, storageclass) +felhom-system/filebrowser-db 100Mi longhorn +felhom-system/filebrowser-files 1Gi longhorn +felhom-system/hub-data 4Gi longhorn +felhom-system/umami-db-data 2Gi longhorn +gitea-system/act-runner-data 5Gi longhorn +gitea-system/gitea-data 50Gi longhorn +vaultwarden-system/vaultwarden-data 5Gi longhorn +=== ArgoCD applications (names, sync policy auto?) +actualbudget auto={"enabled":true} +admin-tools auto={"prune":true,"selfHeal":true} +adventurelog auto={"enabled":true} +arcade auto={"enabled":true} +audiobookshelf auto={"enabled":true} +authentik auto={"prune":true,"selfHeal":true} +booking auto={"enabled":true} +bookstack auto={"enabled":true} +calibre auto={"enabled":true} +cnpg-operator auto={"enabled":false,"prune":true,"selfHeal":true} +code-server auto={"enabled":true} +controller auto={"enabled":true} +crafty auto={"enabled":true} +database-system auto= +external-dns-pihole auto={"enabled":true} +felhom auto={"enabled":false} +fileshare auto={"enabled":true} +gitea auto= +glance auto={"enabled":true} +headlamp auto={"enabled":true} +homepage auto={"enabled":true} +immich auto={"enabled":true} +infrastructure auto= +jarr auto={"enabled":true,"prune":true,"selfHeal":true} +jarr-nightly auto={"enabled":true,"prune":true,"selfHeal":true} +kisfenyo auto={"enabled":true} +monitoring auto= +nextcloud auto={"enabled":true} +office auto={"enabled":true} +opengist auto={"enabled":true} +orsi auto={"enabled":true} +outline auto={"enabled":true} +paperless auto={"enabled":true} +pihole auto={"enabled":false} +plantit auto={"enabled":true} +plex auto={"enabled":true} +privatebin auto={"enabled":true} +root-apps auto={"prune":true,"selfHeal":true} +servarr auto={"enabled":true} +storage auto={"enabled":true} diff --git a/documentation/audits/night-2026-10-23/P2-D30/01a-run1-clock-ahead.txt b/documentation/audits/night-2026-10-23/P2-D30/01a-run1-clock-ahead.txt new file mode 100644 index 00000000..d2b8c4e0 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P2-D30/01a-run1-clock-ahead.txt @@ -0,0 +1,4 @@ +2026-10-22T19:45:26Z real-ish | wg pub 3q1B4aHW… | latest_handshake=1792698270 (now 1792698326, diff -56 s) | prev key file: absent +== clock set +2 h: 2026-10-22T21:45:26Z +== a handshake under the ahead clock at 21:46:32 (epoch 1792705591) +2026-10-22T21:46:32Z real-ish | wg pub 3q1B4aHW… | latest_handshake=1792705591 (now 1792705592, diff -1 s) | prev key file: absent diff --git a/documentation/audits/night-2026-10-23/P2-D30/02a-run1-clock-back.txt b/documentation/audits/night-2026-10-23/P2-D30/02a-run1-clock-back.txt new file mode 100644 index 00000000..f4fd6fb6 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P2-D30/02a-run1-clock-back.txt @@ -0,0 +1,3 @@ +== clock back + chrony: 2026-10-22T19:46:43Z synced=yes +2026-10-22T19:46:43Z real-ish | wg pub 3q1B4aHW… | latest_handshake=1792705591 (now 1792698403, diff 7188 s) | prev key file: absent +tunnel ping: ok diff --git a/documentation/audits/night-2026-10-23/P2-D30/03a-run1-ep0-initiated-heal.txt b/documentation/audits/night-2026-10-23/P2-D30/03a-run1-ep0-initiated-heal.txt new file mode 100644 index 00000000..43bbd2b3 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P2-D30/03a-run1-ep0-initiated-heal.txt @@ -0,0 +1,15 @@ +2026-10-22T19:50:56Z real-ish | wg pub 3q1B4aHW… | latest_handshake=1792698580 (now 1792698657, diff -77 s) | prev key file: absent +interface: wg-felhom + public key: 3q1B4aHW… + listening port: 50300 + +peer: f3d1ZI7/… + endpoint: 167.233.158.164:443 + allowed ips: 10.77.0.1/32, 10.77.0.250/32 + latest handshake: 1 minute, 17 seconds ago + transfer: 4.14 MiB received, 2.44 MiB sent + persistent keepalive: every 25 seconds +tunnel-ping-ok +2026-10-22T23:46:09+02:00 night1023 felhom-agent[35760]: time=2026-10-22T23:46:09.963+02:00 level=WARN msg="wgtunnel: tunnel handshake stale but endpoint IP unchanged — endpoint may be down; not restarting" endpoint_ip=167.233.158.164 +2026-10-22T21:47:09+02:00 night1023 felhom-agent[35760]: time=2026-10-22T21:47:09.791+02:00 level=WARN msg="wgtunnel: the latest handshake is stamped in the future — this box's clock ran ahead, and the endpoint refuses its newer handshakes; re-keying if it s +2026-10-22T21:50:09+02:00 night1023 felhom-agent[35760]: time=2026-10-22T21:50:09.778+02:00 level=INFO msg="wgtunnel: the handshake is no longer stamped in the future — no re-key needed" diff --git a/documentation/audits/night-2026-10-23/P2-D30/04-run2-block-and-ahead.txt b/documentation/audits/night-2026-10-23/P2-D30/04-run2-block-and-ahead.txt new file mode 100644 index 00000000..0f8a2277 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P2-D30/04-run2-block-and-ahead.txt @@ -0,0 +1,5 @@ +-A INPUT -s 167.233.158.164/32 -p udp -m udp --sport 443 -m u32 --u32 "0x0>>0x16&0x3c@0x8=0x1000000" -m comment --comment d30-test -j DROP +2026-10-22T19:52:01Z real-ish | wg pub 3q1B4aHW… | latest_handshake=1792698706 (now 1792698721, diff -15 s) | prev key file: absent +== clock set +2 h: 2026-10-22T21:52:01Z +== a handshake under the ahead clock at 21:54:50 (epoch 1792706086) +2026-10-22T21:54:50Z real-ish | wg pub 3q1B4aHW… | latest_handshake=1792706086 (now 1792706090, diff -4 s) | prev key file: absent diff --git a/documentation/audits/night-2026-10-23/P2-D30/05-run2-clock-back.txt b/documentation/audits/night-2026-10-23/P2-D30/05-run2-clock-back.txt new file mode 100644 index 00000000..021849ea --- /dev/null +++ b/documentation/audits/night-2026-10-23/P2-D30/05-run2-clock-back.txt @@ -0,0 +1,3 @@ +== clock back + chrony: 2026-10-22T19:55:01Z synced=yes +2026-10-22T19:55:01Z real-ish | wg pub 3q1B4aHW… | latest_handshake=1792706086 (now 1792698901, diff 7185 s) | prev key file: absent +tunnel ping: ok diff --git a/documentation/audits/night-2026-10-23/P2-D30/06-run2-after-rekey.txt b/documentation/audits/night-2026-10-23/P2-D30/06-run2-after-rekey.txt new file mode 100644 index 00000000..109a3013 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P2-D30/06-run2-after-rekey.txt @@ -0,0 +1,10 @@ +2026-10-22T20:10:49Z real-ish | wg pub 3q1B4aHW… | latest_handshake=1792706086 (now 1792699849, diff 6237 s) | prev key file: present +tunnel ping: FAILS +2026-10-22T22:10:09+02:00 night1023 felhom-agent[35760]: time=2026-10-22T22:10:09.972+02:00 level=WARN msg="wgtunnel: RE-KEYED after a handshake stamped in the future (D30) — registering the new key; the hub re-keys the peer in place and the endpoint takes the new key" old_pubkey="3q1B4aHW7EohL7Y8 +2026-10-22T22:10:10+02:00 night1023 felhom-agent[35760]: time=2026-10-22T22:10:10.772+02:00 level=INFO msg="wgtunnel: registered with hub" pubkey="HAYwc1CxA7Qb19sd6tlX1Y5YxHM8FeN0Tp8VCa2K8lY=" assigned_ip=10.77.0.5/32 existed=false generation=3 sync=ok +total 20 +-rw------- last-applied.sha +-rw------- private.key +-rw------- private.key.prev +-rw------- registered.json +-rw------- wg-felhom.conf diff --git a/documentation/audits/night-2026-10-23/P2-D30/07-run2-new-key-handshake.txt b/documentation/audits/night-2026-10-23/P2-D30/07-run2-new-key-handshake.txt new file mode 100644 index 00000000..4e21495f --- /dev/null +++ b/documentation/audits/night-2026-10-23/P2-D30/07-run2-new-key-handshake.txt @@ -0,0 +1,9 @@ +2026-10-22T20:16:04Z real-ish | wg pub HAYwc1Cx… | latest_handshake=1792700136 (now 1792700164, diff -28 s) | prev key file: present +tunnel ping: ok +2026-10-22T22:10:09+02:00 night1023 felhom-agent[35760]: time=2026-10-22T22:10:09.972+02:00 level=WARN msg="wgtunnel: RE-KEYED after a handshake stamped in the future (D30) — registering the new key; the hub re-keys the peer in place and the endpoint takes the new key" old_pubkey="3q1B4aHW7EohL7Y8qbE5Z0lbo21kqT3mocvigZdPrBg=" +2026-10-22T22:10:10+02:00 night1023 felhom-agent[35760]: time=2026-10-22T22:10:10.772+02:00 level=INFO msg="wgtunnel: registered with hub" pubkey="HAYwc1CxA7Qb19sd6tlX1Y5YxHM8FeN0Tp8VCa2K8lY=" assigned_ip=10.77.0.5/32 existed=false generation=3 sync=ok +2026-10-22T22:11:09+02:00 night1023 felhom-agent[35760]: time=2026-10-22T22:11:09.867+02:00 level=WARN msg="wgtunnel: the latest handshake is stamped in the future — this box's clock ran ahead, and the endpoint refuses its newer handshakes; re-keying if it stays" future_s=6217 rekey_after=15m0s +2026-10-22T22:15:36+02:00 night1023 felhom-agent[35760]: time=2026-10-22T22:15:36.774+02:00 level=INFO msg="wgtunnel: tunnel conf applied" endpoint=167.233.158.164:443 dns_name=ep0.felhom.eu assigned_ip=10.77.0.5/32 action=restart +2026-10-22T22:15:36+02:00 night1023 felhom-agent[35760]: time=2026-10-22T22:15:36.849+02:00 level=INFO msg="wgtunnel: the handshake is no longer stamped in the future — no re-key needed" +(re-registers while the served block still named the old key, once a minute, existed=true: 5) +rules left: 0 diff --git a/documentation/audits/night-2026-10-23/P2-D30/08-hub-db-read.txt b/documentation/audits/night-2026-10-23/P2-D30/08-hub-db-read.txt new file mode 100644 index 00000000..396128c1 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P2-D30/08-hub-db-read.txt @@ -0,0 +1,11 @@ +== hub DB read (named non-secret columns), host Tester4-16fdef +wg_peers cols: ['pubkey', 'assigned_ip', 'host_id', 'note', 'created_at', 'updated_at'] +wg_peer ('Tester4-16fdef', 'HAYwc1Cx…', '10.77.0.5', '2026-10-22 20:10:10') +host ('Tester4-16fdef', 3, '2026-10-22 20:15:36') +host_reports cols: ['id', 'host_id', 'customer_id', 'received_at', 'report_json', 'agent_version', 'cpu_percent', 'memory_percent', 'disk_percent', 'guest_total', 'guest_running', 'cloudflared_status'] +2026-10-22 19:13:31 wg: {'pubkey': '', 'active': False, 'last_handshake_age_s': None, 'handshake_future_s': None} +2026-10-22 19:28:36 wg: {'pubkey': '3q1B4aHW…', 'active': True, 'last_handshake_age_s': 14, 'handshake_future_s': None} +2026-10-22 19:30:14 wg: {'pubkey': '3q1B4aHW…', 'active': True, 'last_handshake_age_s': 112, 'handshake_future_s': None} +2026-10-22 19:45:20 wg: {'pubkey': '3q1B4aHW…', 'active': True, 'last_handshake_age_s': 50, 'handshake_future_s': None} +2026-10-22 20:00:35 wg: {'pubkey': '3q1B4aHW…', 'active': True, 'last_handshake_age_s': 0, 'handshake_future_s': 6851} +2026-10-22 20:15:36 wg: {'pubkey': 'HAYwc1Cx…', 'active': True, 'last_handshake_age_s': 0, 'handshake_future_s': 5950} diff --git a/documentation/audits/night-2026-10-23/P2-D30/README.md b/documentation/audits/night-2026-10-23/P2-D30/README.md new file mode 100644 index 00000000..9c5882e3 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P2-D30/README.md @@ -0,0 +1,18 @@ +# Part 2 — D30 live: clock ahead → (alarm input) → re-key → handshake + +Box: test box Tester4-16fdef (nested VM 9344 on demo-hp, installed tonight from ISO 1.30.0), agent fixture +`0.163.0-night.1` = felhom-agent `ae69ad4` (D30). Hub: the live 0.165.0 (no alarm yet — the alarm and its clearing are +pinned by hub tests, `84751cb0`, not deployed). Tool: `d30-box.sh` (in the scratchpad; state lines print 8 characters +of a PUBLIC key, never a private key). + +| File | What | +|---|---| +| `01a-run1-clock-ahead.txt`, `02a-run1-clock-back.txt`, `03a-run1-ep0-initiated-heal.txt` | Run 1: clock +2 h → a handshake stamped +2 h; clock back → the agent saw the future stamp (21:47) and, 3 min later, the stamp was in the past: **ep0 started a handshake of its own** (it had traffic for the box; the box accepts ep0's timestamps), the tunnel healed, the agent correctly did not re-key | +| `04-run2-block-and-ahead.txt`, `05-run2-clock-back.txt` | Run 2, the rehearsal's condition: ep0's handshake starts dropped on the box (iptables `u32` on WireGuard message type 1, comment `d30-test`); clock +2 h, back | +| `06-run2-after-rekey.txt` | the tunnel dead; at 22:10:09 local **RE-KEYED** (old `3q1B4aHW…` → new `HAYwc1Cx…`), the hub: `existed=false generation=3 sync=ok`, same 10.77.0.5; the old key kept as `private.key.prev` | +| `07-run2-new-key-handshake.txt` | 22:15:36 local the desired state carried the new key → conf applied → **a handshake with the new key**, tunnel ping ok; the test rule removed (rules left: 0) | +| `08-hub-db-read.txt` | the hub (named columns): `wg_peers` holds the new key for the host since 20:10:10Z; the stored reports show `handshake_future_s=6851` at 20:00:35Z (the alarm's input) | + +Timeline (UTC): clock back 19:55:01 → future stamp seen 19:55:09 → re-key 20:10:09 → hub took it 20:10:10 → the box +switched 20:15:36 (its next desired-state fetch) → handshake 20:15:36. Outage after the clock was right: ~20 min (the +rehearsal's: until 2028 or a hand reset on ep0). diff --git a/documentation/audits/night-2026-10-23/P3-review/felhom-agent-bundle-D4-v2.patch b/documentation/audits/night-2026-10-23/P3-review/felhom-agent-bundle-D4-v2.patch new file mode 100644 index 00000000..df09c4d6 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P3-review/felhom-agent-bundle-D4-v2.patch @@ -0,0 +1,222 @@ +diff --git a/configs/felhom-os-apply b/configs/felhom-os-apply +index 81565fb..ee8429f 100755 +--- a/configs/felhom-os-apply ++++ b/configs/felhom-os-apply +@@ -165,7 +165,8 @@ KERNEL_ACTIVE = ("staged", "oneshot", "judging", "reverting") + # dest: (source file under configs/, mode, check, policy) + # check: the content check BEFORE anything is written (R18): sudoers → visudo -cf; sh / bash → -n; python → compile; + # unit → a [Unit]/[Service]/[Timer] section; unit-nort → that, and never RuntimeDirectory= (the G1 incident); +-# agent-unit → User=felhom-agent; dropin → a [Unit] section (SF-3); nft → nft -c -f; plain → none. ++# agent-unit → User=felhom-agent; dropin → a [Unit] section (SF-3); nft → nft -c -f; plain → none; ++# sshd → `sshd -t -f` on the snippet alone (R-982). + # policy: replace → always written when it differs; if-absent → only when the box has none (a setting the operator may + # have tuned); oob → only on a box with the OOB belt (/etc/felhom-sshd exists — the installer's --no-oob leaves none). + # Order matters: the sudoers files come LAST, so a referenced wrapper is in place before the line that allows it. +@@ -184,6 +185,9 @@ BUNDLE_KEEP = 3 + BUNDLE_MAX_BYTES = 4 * 1024 * 1024 + BUNDLE_RE = re.compile(r"^bundle-[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?\.json$") + OOB_DIR = "/etc/felhom-sshd" ++# R-982: the host's own sshd drop-in (after_install reloads sshd for it; undo reloads after putting it back). ++SSHD_DROPIN = "/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf" ++ + BUNDLE_FILES = [ + ("/usr/local/sbin/felhom-mkfs-guarded", "felhom-mkfs-guarded.sh", 0o755, "bash", "replace"), + ("/usr/local/sbin/felhom-selfupdate-guarded", "felhom-selfupdate-guarded", 0o755, "sh", "replace"), +@@ -221,6 +225,9 @@ BUNDLE_FILES = [ + ("/etc/systemd/system/felhom-sshd.service", "felhom-sshd.service", 0o644, "unit-nort", "oob"), + ("/etc/felhom-oob.nft", "felhom-oob.nft", 0o644, "nft", "oob"), + ("/etc/systemd/system/felhom-oob-nft.service", "felhom-oob-nft.service", 0o644, "unit", "oob"), ++ # R-982, operator ruling D4 (2026-10-22): the host's own sshd (port 22, the LAN): root by key only. The console ++ # password stays; felhom-sshd (the operator's door) reads nothing from sshd_config.d. Reloaded in after_install. ++ ("/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf", "felhom-root-ssh-key-only.conf", 0o644, "sshd", "replace"), + ("/etc/sudoers.d/felhom-op", "felhom-op.sudoers", 0o440, "sudoers", "oob"), + ("/etc/sudoers.d/felhom-agent", "felhom-agent.sudoers", 0o440, "sudoers", "replace"), + ] +@@ -434,7 +441,8 @@ class Runner: + def check_content(self, kind, data): + """Run an external syntax check on a root-only temp copy. Returns (rc, message).""" + import tempfile +- cmd = {"sudoers": ["visudo", "-cf"], "sh": ["sh", "-n"], "bash": ["bash", "-n"], "nft": ["nft", "-c", "-f"]}[kind] ++ cmd = {"sudoers": ["visudo", "-cf"], "sh": ["sh", "-n"], "bash": ["bash", "-n"], "nft": ["nft", "-c", "-f"], ++ "sshd": ["/usr/sbin/sshd", "-t", "-f"]}[kind] + with tempfile.TemporaryDirectory(prefix="felhom-bundle-") as d: + p = os.path.join(d, "f") + with open(p, "wb") as f: +@@ -1864,6 +1872,7 @@ class Bundle: + def __init__(self, apply): + self.a, self.r = apply, apply.r + self.report = apply.report ++ self.sshd_dropin_not_in_force = False + + # ---------- reading and checking ---------- + def load_file(self, path): +@@ -1932,10 +1941,17 @@ class Bundle: + text = content.decode("utf-8") + except UnicodeDecodeError: + raise Refused("R18", f"{dest} is not UTF-8 text") +- if check in ("sudoers", "sh", "bash", "nft"): ++ if check in ("sudoers", "sh", "bash", "nft", "sshd"): + rc, msg = self.r.check_content(check, content) + if rc != 0: + raise Refused("R18", f"{dest} fails its {check} check: {msg}") ++ if check == "sshd": ++ # R-982: the drop-in's PermitRootLogin must be exactly `prohibit-password`. `no` would also shut the root key ++ # out (Proxmox's own node-to-node SSH and the LAN key route), `yes` would undo the ruling — both refused. ++ # Only that line is pinned: the file is signed like every bundle file (review A7). ++ vals = re.findall(r"^\s*PermitRootLogin\s+(\S+)\s*$", text, re.M | re.I) ++ if vals != ["prohibit-password"]: ++ raise Refused("R18", f"{dest} must set exactly `PermitRootLogin prohibit-password` (found {vals})") + elif check == "python": + try: + compile(text, dest, "exec") +@@ -2044,6 +2060,8 @@ class Bundle: + self.report["failed"] = {"rc": 3, "step": "bundle", "reason": reason[:300]} + return 3 + rep["signers_created"] = False ++ if self.sshd_dropin_not_in_force: ++ rep["sshd_dropin_not_in_force"] = True # R-982 A2: written and reloaded, but sshd -T shows root passwords allowed + if bootstrap and not self.r.lexists(TRUST_SIGNERS): + self.r.put_file(TRUST_SIGNERS, pinned_signers_line().encode(), 0o644) + rep["signers_created"] = True +@@ -2073,6 +2091,23 @@ class Bundle: + "enable the crash guard") + if "/etc/systemd/system/felhom-mgmt-watchdog.timer" in dests: + self.must(["systemctl", "enable", "--now", "felhom-mgmt-watchdog.timer"], "enable the mgmt watchdog timer") ++ # R-982: the host's sshd takes the root-key-only drop-in. The WHOLE config is tested first (a failure is a ++ # refusal, and the bundle undo puts the old files back); `try-reload-or-restart` keeps every open session and ++ # does nothing on a box whose ssh.service is not running (a socket-activated or stopped sshd reads the file ++ # at its next start) — a failure there must not freeze the bundle route (review 2026-10-22 night, A1). ++ # felhom-sshd is not touched. Then the POSITIVE check: sshd's own effective view for root. A box whose ++ # sshd_config has no Include of sshd_config.d, an earlier drop-in or a Match block can keep the password route ++ # — that is said loudly in the log and the report, never silently recorded as done (A2). ++ if SSHD_DROPIN in written: ++ self.must(["/usr/sbin/sshd", "-t"], "sshd -t (the host's whole sshd config)") ++ self.must(["systemctl", "try-reload-or-restart", "ssh"], "systemctl try-reload-or-restart ssh") ++ rc, out, err = self.r.host(["/usr/sbin/sshd", "-T", "-C", "user=root,host=felhom-check,addr=192.0.2.1"], 60) ++ eff = re.findall(r"^permitrootlogin\s+(\S+)", out, re.M | re.I) ++ if rc != 0 or eff != ["without-password"]: ++ self.r.log(f"os-apply: BUNDLE WARNING the root-key-only drop-in is NOT in force: sshd -T says " ++ f"permitrootlogin={eff or '?'} (rc={rc}) — check sshd_config's Include, an earlier drop-in " ++ f"or a Match block (R-982)") ++ self.sshd_dropin_not_in_force = True + + def must(self, argv, what): + rc, out, err = self.r.host(argv, 120) +@@ -2133,6 +2168,9 @@ class Bundle: + self.r.log(f"os-apply: BUNDLE UNDO could not restore {dest}: {e}") + if any(d.startswith("/etc/systemd/system/") for d in back): + self.r.host(["systemctl", "daemon-reload"], 120) ++ if SSHD_DROPIN in back: ++ # A3: after_install may already have reloaded sshd with the drop-in; the restored state takes effect now. ++ self.r.host(["systemctl", "try-reload-or-restart", "ssh"], 120) + rc, _, _ = self.r.host(["visudo", "-c"], 60) + self.r.log(f"os-apply: BUNDLE UNDO restored={len(back)} visudo-after={'ok' if rc == 0 else 'FAILED'}") + return back +diff --git a/configs/felhom-root-ssh-key-only.conf b/configs/felhom-root-ssh-key-only.conf +new file mode 100644 +index 0000000..d92eea9 +--- /dev/null ++++ b/configs/felhom-root-ssh-key-only.conf +@@ -0,0 +1,7 @@ ++# MANAGED BY FELHOM (the agent's config bundle) — R-982, operator ruling D4 (2026-10-22). ++# The Proxmox host's OWN sshd — the listener the household network reaches on port 22: root logs in with a KEY only. ++# The root password still works on the console (the break-glass password: felhom.eu runbooks/break-glass.md). The ++# operator's door (felhom-sshd, port 8822, /etc/felhom-sshd/sshd_config) is a separate daemon whose config includes ++# nothing from here, so it is not changed. Debian's sshd_config reads this folder BEFORE its own settings, and for ++# sshd the first value wins; the bundle checks the effective value with `sshd -T` after the reload. ++PermitRootLogin prohibit-password +diff --git a/configs/test_felhom_config_bundle.py b/configs/test_felhom_config_bundle.py +index 1c5a6ab..58b5238 100644 +--- a/configs/test_felhom_config_bundle.py ++++ b/configs/test_felhom_config_bundle.py +@@ -429,6 +429,90 @@ class SelfCheckUndo(unittest.TestCase): + self.assert_restored(box, before, rep) + + ++ ++class RootKeyOnly(unittest.TestCase): ++ """R-982, operator ruling D4 (2026-10-22): the bundle gives the host's own sshd a root-key-only drop-in, tests the ++ whole config and reloads sshd — only when the file was written; a drop-in that says anything but ++ `prohibit-password` is refused before anything is written. ++ RED-PROOF: with the after_install block removed, test_fresh_box_takes_the_dropin_and_reloads fails (no reload); ++ with the content rule removed, test_a_dropin_that_says_yes_or_no_is_refused fails — seen failing 2026-10-22.""" ++ DEST = "/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf" ++ ++ def test_fresh_box_takes_the_dropin_and_reloads(self): ++ data, sha = real_bundle() ++ box = Box(data, signed_job(sha)) ++ rc, rep = run(box) ++ self.assertEqual(rc, 0, rep) ++ self.assertIn(b"PermitRootLogin prohibit-password", box.files[self.DEST]) ++ self.assertEqual(box.modes[self.DEST], 0o644) ++ self.assertIn(["/usr/sbin/sshd", "-t"], box.calls) ++ self.assertIn(["systemctl", "try-reload-or-restart", "ssh"], box.calls) ++ self.assertLess(box.calls.index(["/usr/sbin/sshd", "-t"]), box.calls.index(["systemctl", "try-reload-or-restart", "ssh"])) ++ self.assertFalse(any("felhom-sshd" in " ".join(c) for c in box.calls), "the operator's door must not be touched") ++ ++ def test_an_unchanged_dropin_is_not_reloaded(self): ++ data, sha = real_bundle() ++ box = Box(data, signed_job(sha)) ++ box.put(self.DEST, (REPO / "configs" / "felhom-root-ssh-key-only.conf").read_bytes(), 0o644) ++ rc, rep = run(box) ++ self.assertEqual(rc, 0, rep) ++ self.assertNotIn(["systemctl", "try-reload-or-restart", "ssh"], box.calls) ++ ++ def test_a_dropin_that_says_yes_or_no_is_refused(self): ++ for bad in (b"PermitRootLogin yes\n", b"PermitRootLogin no\n", b"# nothing\n", ++ b"PermitRootLogin prohibit-password\nPermitRootLogin yes\n"): ++ def edit(files, bad=bad): ++ for e in files: ++ if e["path"] == self.DEST: ++ e["content_b64"] = base64.b64encode(bad).decode() ++ data, sha = edited_bundle(edit) ++ box = Box(data, signed_job(sha)) ++ rc, rep = run(box) ++ self.assertNotEqual(rc, 0, bad) ++ self.assertNotIn(self.DEST, box.files, bad) ++ ++ # Review 2026-10-22 night. A2: the effective value is read back with `sshd -T`; a drop-in that is not in force is ++ # said in the log and the report (not refused — a refusal would freeze the bundle route). A3: a bundle undone ++ # after the reload reloads sshd again with the old state. RED-PROOF: with the sshd -T check removed, ++ # test_a_dropin_not_in_force_is_reported fails; with the undo reload removed, test_an_undone_bundle_reloads_sshd ++ # fails — seen failing 2026-10-22 night. ++ def _sshd_T(self, box, value): ++ orig = box.host ++ def host(argv, timeout=600, stdin=None): ++ if argv[:2] == ["/usr/sbin/sshd", "-T"]: ++ box.calls.append(argv) ++ return 0, f"port 22\npermitrootlogin {value}\npasswordauthentication yes\n", "" ++ return orig(argv, timeout, stdin) ++ box.host = host ++ ++ def test_the_effective_value_is_checked(self): ++ data, sha = real_bundle() ++ box = Box(data, signed_job(sha)) ++ self._sshd_T(box, "without-password") ++ rc, rep = run(box) ++ self.assertEqual(rc, 0, rep) ++ self.assertTrue(any(c[:2] == ["/usr/sbin/sshd", "-T"] for c in box.calls)) ++ self.assertNotIn("sshd_dropin_not_in_force", rep["bundle"]) ++ ++ def test_a_dropin_not_in_force_is_reported(self): ++ data, sha = real_bundle() ++ box = Box(data, signed_job(sha)) ++ self._sshd_T(box, "yes") # e.g. no Include of sshd_config.d ++ rc, rep = run(box) ++ self.assertEqual(rc, 0, rep) ++ self.assertTrue(rep["bundle"].get("sshd_dropin_not_in_force"), rep) ++ self.assertTrue(any("NOT in force" in l for l in box.logs), box.logs) ++ ++ def test_an_undone_bundle_reloads_sshd(self): ++ data, sha = real_bundle() ++ box = Box(data, signed_job(sha)) ++ box.visudo_fail = True # the self-check fails AFTER the install → undo ++ rc, rep = run(box) ++ self.assertEqual(rc, 3, rep) ++ self.assertNotIn(self.DEST, box.files) ++ reloads = [i for i, c in enumerate(box.calls) if c == ["systemctl", "try-reload-or-restart", "ssh"]] ++ self.assertEqual(len(reloads), 2, box.calls) # after_install, then the undo ++ + class TrustBootstrap(unittest.TestCase): + def test_missing_signers_verifies_against_the_pinned_key_and_creates_it(self): + data, sha = real_bundle() diff --git a/documentation/audits/night-2026-10-23/P3-review/felhom.eu-installer-D4-v2.patch b/documentation/audits/night-2026-10-23/P3-review/felhom.eu-installer-D4-v2.patch new file mode 100644 index 00000000..d9b82981 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P3-review/felhom.eu-installer-D4-v2.patch @@ -0,0 +1,29 @@ +diff --git a/scripts/felhom-host-install.sh b/scripts/felhom-host-install.sh +index 970109fd..349da0c5 100644 +--- a/scripts/felhom-host-install.sh ++++ b/scripts/felhom-host-install.sh +@@ -1417,6 +1417,12 @@ run_uninstall() { + if [[ -f /etc/default/grub.d/zz-felhom-kernel-default.cfg ]]; then + log_info " kept /etc/default/grub.d/zz-felhom-kernel-default.cfg (the kernel this box booted healthily stays the default)" + fi ++ # R-982 (operator ruling D4, 2026-10-22; agent bundle after v0.162.0): the host sshd's root-key-only drop-in. The ++ # uninstall gives the host back as Proxmox set it up, so the drop-in goes and sshd is reloaded (open sessions stay). ++ if [[ -f /etc/ssh/sshd_config.d/10-felhom-root-key-only.conf ]]; then ++ run rm -f /etc/ssh/sshd_config.d/10-felhom-root-key-only.conf ++ run systemctl try-reload-or-restart ssh 2>/dev/null || log_warn " sshd was not reloaded — the drop-in is gone, sshd reads it at its next start (R-982)" ++ fi + # 1.31.0 (R-840): the bundle's previous copies and the wrapper's nonce record. + if [[ -d /var/lib/felhom-os-apply ]]; then run rm -rf /var/lib/felhom-os-apply; fi + if [[ -f /var/lib/vz/snippets/felhom-guest-hook.sh ]]; then run rm -f /var/lib/vz/snippets/felhom-guest-hook.sh; fi +diff --git a/scripts/test_hostinstall.py b/scripts/test_hostinstall.py +index 3f7088a6..e9cf9ad9 100644 +--- a/scripts/test_hostinstall.py ++++ b/scripts/test_hostinstall.py +@@ -60,6 +60,7 @@ BUNDLE_DESTS = [ + "/etc/systemd/system/felhom-sshd.service", + "/etc/felhom-oob.nft", + "/etc/systemd/system/felhom-oob-nft.service", ++ "/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf", # R-982, operator ruling D4 (2026-10-22) + "/etc/sudoers.d/felhom-op", + "/etc/sudoers.d/felhom-agent", + ] diff --git a/documentation/audits/night-2026-10-23/P4-testnight/01-bind.txt b/documentation/audits/night-2026-10-23/P4-testnight/01-bind.txt new file mode 100644 index 00000000..9bc248e9 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P4-testnight/01-bind.txt @@ -0,0 +1,4 @@ +GET HTTP/2 200 +fields: ['pairing_code', 'passphrase', 'robots', 'viewport'] +POST HTTP/2 200 +answer: Felhom — Doboz összekötése Felhom doboz összekötése Sikeres összekötés. A doboz kb. egy percen belül folytatja a telepítést. Ezt az oldalt bezárhatod — a beállítás a háttérben befejeződik, és a vezérlőpultod hamarosan elérhető lesz. Felhom.eu diff --git a/documentation/audits/night-2026-10-23/P4-testnight/02-claim.txt b/documentation/audits/night-2026-10-23/P4-testnight/02-claim.txt new file mode 100644 index 00000000..5304ff61 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P4-testnight/02-claim.txt @@ -0,0 +1,3 @@ +GET /claim HTTP/2 200 +csrf_len 64 cookie_present yes +POST /claim HTTP/2 302 location: / diff --git a/documentation/audits/night-2026-10-23/P4-testnight/03-agent-and-d4-fixture.txt b/documentation/audits/night-2026-10-23/P4-testnight/03-agent-and-d4-fixture.txt new file mode 100644 index 00000000..a60fe949 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P4-testnight/03-agent-and-d4-fixture.txt @@ -0,0 +1,17 @@ +== 19:30:07 agent fixture +felhom-agent 0.163.0-night.1 +active +ac39dadd162bc74f +== 19:30:11 D4 drop-in by hand (what the bundle after_install does: sshd -t -f, install, sshd -t, try-reload-or-restart, sshd -T) +snippet check rc=0 +sshd -t ok +reloaded +permitrootlogin without-password +passwordauthentication yes +== 19:30:28 D4 checks from DooPlex (LAN) +root PASSWORD over SSH: Permission denied, please try again. +root KEY over SSH: root +web page password login (/api2/json/access/ticket): HTTP 200 +== 19:30:43 controller fixture +gitea.dooplex.hu/admin/felhom-controller:0.340.0-rc.6 Up 15 seconds (healthy) +2026/10/22 19:30:45 [INFO] [tier2] start-up: no leftover second-drive copy container diff --git a/documentation/audits/night-2026-10-23/P4-testnight/04-drive-init.txt b/documentation/audits/night-2026-10-23/P4-testnight/04-drive-init.txt new file mode 100644 index 00000000..c2fed5f2 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P4-testnight/04-drive-init.txt @@ -0,0 +1,10 @@ +== 19:32:04 POST /api/storage/init device=/dev/sdb fstype=ext4 mount_name=hdd_1 +200 +{ + "data": { + "phase": "formatting", + "started": true + }, + "ok": true +} +200{"data":{"device":"/dev/sdb","durable_id":"","error":"","opsign":"","phase":"done","reason":"","started_at":"2026-10-22T19:32:09.922168725Z","updated_at":"2026-10-22T19:32:12.640993967Z","where":"/mnt/felhom-drives/hdd_1"},"ok":true} diff --git a/documentation/audits/night-2026-10-23/P4-testnight/05-installs.txt b/documentation/audits/night-2026-10-23/P4-testnight/05-installs.txt new file mode 100644 index 00000000..8ef7b680 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P4-testnight/05-installs.txt @@ -0,0 +1,17 @@ +19:32:39Z nextcloud deploy 202 Telepítés elindítva – az állapot a kártyán követhető +19:34:09Z nextcloud → running 97 s +19:34:10Z immich deploy 202 Telepítés elindítva – az állapot a kártyán követhető +19:36:31Z immich → running 141 s +19:36:31Z paperless-ngx deploy 202 Telepítés elindítva – az állapot a kártyán követhető +19:38:41Z paperless-ngx → running 130 s +19:38:41Z vaultwarden deploy 202 Telepítés elindítva – az állapot a kártyán követhető +19:39:01Z vaultwarden → running 20 s +19:39:02Z mealie deploy 202 Telepítés elindítva – az állapot a kártyán követhető +19:40:32Z mealie → running 90 s +19:40:33Z actualbudget deploy 202 Telepítés elindítva – az állapot a kártyán követhető +19:41:03Z actualbudget → running 30 s +19:41:03Z radicale deploy 202 Telepítés elindítva – az állapot a kártyán követhető +19:41:33Z radicale → running 30 s +19:41:33Z bookstack deploy 202 Telepítés elindítva – az állapot a kártyán követhető +19:42:43Z bookstack → running 70 s +19:42:43Z memory: {"data": {"vmid": 9201, "allocated_mb": 4096, "usage_mb": 2126, "host_total_mb": 7939, "min_mb": 2048, "max_mb": 5891, "floor_mb": 2638, "running": true, "host_swap_mb": 7935, "guest_swap_mb": 4096}, "ok": true} diff --git a/documentation/audits/night-2026-10-23/P4-testnight/06-loader-ct.txt b/documentation/audits/night-2026-10-23/P4-testnight/06-loader-ct.txt new file mode 100644 index 00000000..61e4c7a0 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P4-testnight/06-loader-ct.txt @@ -0,0 +1,7 @@ +WARN: Systemd 257 detected. You may need to enable nesting. +Task finished with 1 warning(s)! +hostname: night1023-loader +memory: 2048 +rootfs: nvme-scratch:9345/vm-9345-disk-0.raw,size=24G +192.168.0.152 +19:39:56 diff --git a/documentation/audits/night-2026-10-23/P4-testnight/install/00-vm-create.txt b/documentation/audits/night-2026-10-23/P4-testnight/install/00-vm-create.txt new file mode 100644 index 00000000..f0590141 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P4-testnight/install/00-vm-create.txt @@ -0,0 +1,22 @@ +Formatting '/mnt/hdd_1/images/9344/vm-9344-disk-0.raw', fmt=raw size=274877906944 preallocation=off +scsi0: successfully created disk 'nvme-scratch:9344/vm-9344-disk-0.raw,size=256G' +Formatting '/mnt/hdd_1/images/9344/vm-9344-disk-1.raw', fmt=raw size=214748364800 preallocation=off +scsi1: successfully created disk 'nvme-scratch:9344/vm-9344-disk-1.raw,size=200G' +update VM 9344: -boot order=scsi0;ide2 +18:46:17 +boot: order=scsi0;ide2 +cores: 4 +cpu: host +ide2: local:iso/felhom-installer-1.30.0-pve9.2-1.iso,media=cdrom,size=1665356K +memory: 8192 +meta: creation-qemu=11.0.3,ctime=1792694775 +name: night1023-soak +net0: virtio=BC:24:11:AB:3A:21,bridge=vmbr0 +ostype: l26 +scsi0: nvme-scratch:9344/vm-9344-disk-0.raw,size=256G +scsi1: nvme-scratch:9344/vm-9344-disk-1.raw,size=200G +scsihw: virtio-scsi-single +smbios1: uuid=cbc4fbdd-86b6-4050-9f10-7f4319c9f86b +vga: std +vmgenid: aa1b5848-cd99-43d0-ae66-f222698ee0ab +Install pressed 18:51:58 diff --git a/documentation/audits/night-2026-10-23/P4-testnight/timeline.txt b/documentation/audits/night-2026-10-23/P4-testnight/timeline.txt new file mode 100644 index 00000000..aab70ef1 --- /dev/null +++ b/documentation/audits/night-2026-10-23/P4-testnight/timeline.txt @@ -0,0 +1,4 @@ +bind POST 19:12:17 +claim 19:29:40 +env written: IRISZ ODON COOKIE +soak started 19:55:59 diff --git a/documentation/runbooks/total-loss-of-dooplex.md b/documentation/runbooks/total-loss-of-dooplex.md index 13ec8d4c..5eb78224 100644 --- a/documentation/runbooks/total-loss-of-dooplex.md +++ b/documentation/runbooks/total-loss-of-dooplex.md @@ -69,7 +69,7 @@ local restic repos; Prometheus history; DooPlex's other homelab apps (not Felhom 7. **Gitea:** `gitea-restore.md` (on a normal network this time). Then rebuild the images from the code into the new registry: hub, controller, agent (`RUNBOOK-manual-build.md`). 8. **The hub:** k3s, then `RUNBOOK-hub-db-offsite-backup.md` §3 with S2 and S4 — **start it with mail held** - (§3 step 1: the `MAIL-HOLD` marker, from the next hub release; until then: no network until the pending notices are understood). + (§3 step 1: the `MAIL-HOLD` marker — shipped in hub 0.145.0, 2026-10-10). 9. **DNS:** in Cloudflare (S11) point `hub.felhom.eu` (today a CNAME to `dooplex.hopto.org`) and `gitea.dooplex.hu` at the new place. The boxes reconnect by themselves: their API keys are in the restored hub DB. 10. **Signing:** the keys are in the restored copy (`signing/`) and on the sheet (S6) — put them back and sign as before. Without it, no box accepts an agent update or a