170d090e7b
gates / gates (push) Successful in 6m59s
Docs 02/05/07/13 and capability F12 (PROVEN-LIVE); the audit's C and D sections; R-1020 (the duration reads 0) and R-1021 (the hub build builds a stale checkout) filed; STATUS one screen. Register 133 -> 140 (this session: opened 2, closed 1; another session added 6). Both plugs ON. Report: REPORT-immich-repair-2026-10-14.md. Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
83 lines
7.3 KiB
Markdown
83 lines
7.3 KiB
Markdown
# Architecture Part 13 — Threat model
|
|
|
|
> **Status: draft** (security review 2026-10-12, `audits/security-review-2026-10-12/`).
|
|
> Marks as in `01`: **[DESIGN]** a decision taken; **[FACT]** an observation with a citation. Unmarked = not yet
|
|
> classified. Findings named here as `R-…` are register rows; for open ones the detail is held off-git.
|
|
|
|
## 1. Starting fact
|
|
|
|
**[FACT]** The controller, agent, felhom.eu and catalog repositories are readable by anyone without a login
|
|
(measured 2026-10-12; `homelab-manifests` is private). **Assume the attacker has read all product source.** A guard
|
|
that depends on the code being secret is not a guard here. Whether the repositories stay public is the operator's
|
|
decision (R-925).
|
|
|
|
## 2. Assets, most harmful loss first
|
|
|
|
1. **Household app data** — photos, documents, passwords in the household's apps (the customer guest, its data drive,
|
|
and the backups).
|
|
2. **Household logins** — the dashboard password, family members, each app's admin.
|
|
3. **Box credentials** — the box's hub API key, the local-API token, the tunnel token, the agent's Proxmox token, the
|
|
vaulted break-glass root credential.
|
|
4. **Hub data** — customer records and reports, escrowed (sealed) backup keys, Cloudflare tokens, the global key.
|
|
5. **Operator signing keys** — they decide what a box accepts (R-924).
|
|
6. **Off-site backups and their keys** — ciphertext on ep0 and the Storage Box.
|
|
7. **Gitea, the registry and the installer** — they decide what code runs on every box (R-971, R-979, R-980).
|
|
|
|
## 3. Attackers and what they start with
|
|
|
|
| | attacker | starts with |
|
|
|---|---|---|
|
|
| a | a stranger on the internet | the public source; the tunnel addresses of every app and dashboard; the hub's public name (from an allowed country) |
|
|
| b | someone on the household LAN | the box's LAN address; the host's and the guest's LAN ports (measured: SSH, the Proxmox UI, DNS, rpcbind) |
|
|
| c | a household member with a family account | a family session |
|
|
| d | a bad catalog app (upstream image turned bad) | code in one container on the guest's shared Docker daemon |
|
|
| e | a compromised controller | root in the guest, the docker socket, the local-API token, the box's hub key |
|
|
| f | a compromised hub (or a copy of its database) | every box's hub key, sealed escrow blobs, Cloudflare tokens |
|
|
| g | a compromised Gitea / registry / build | push to the catalog, the images and the installer tag |
|
|
| h | a thief with the box | the disks |
|
|
| i | Cloudflare | the plaintext of tunnel traffic — **accepted** (R-904) |
|
|
|
|
## 4. Entry points
|
|
|
|
**How enumerated (re-runnable):** none of the three uses a router library; each registers a few `ServeMux` patterns
|
|
and dispatches in `switch` blocks, so the cases were parsed from source, and auth was read by hand per handler. The
|
|
scripts are held with the review's working notes.
|
|
|
|
| component | routes | no credential | listeners |
|
|
|---|---|---|---|
|
|
| controller | 231 (+8 in setup mode) | 21, of which 7 change state, each behind its own password, code or token | port 8080 on the guest's Docker networks (not published); mail relay 2525/2465/2526 |
|
|
| hub | 119 (1 health, 53 API, 65 web) | appliance register (rate-limited, by design), re-enroll (only while the operator has armed recovery mode), login, health | one ingress, path `/`, geo filter (HU, IE, LAN) |
|
|
| agent | 38 local-API routes (`POST /host/poweroff` added 2026-10-13, R-998) | 0 | local API on the island bridge; WireGuard; poke (tunnel only); felhom-sshd (tunnel only, measured) |
|
|
|
|
**Outbound fetches that bring code or config onto a box:** the catalog (git, `main`), app images, the four infra
|
|
images, the controller image (by tag), `controller.yaml` from the hub, the agent binary and config bundle (signed),
|
|
OS packages (exact versions, apt-signed), the installer script (by tag, R-980).
|
|
|
|
## 5. The trust table of `01` §5, checked
|
|
|
|
| boundary | verdict | evidence |
|
|
|---|---|---|
|
|
| end-user ↔ apps | holds, with gaps | traefik routing and header stripping hold; homepage has the Docker socket and no gate (R-981) |
|
|
| customer ↔ controller UI | partly | bcrypt and session hold; small gaps (R-983); family sessions survive a reset (R-976) |
|
|
| controller ↔ agent | partly | every route bound to the caller's guest; a compromised controller can still flush restore points (R-973), void the recovery code (R-978), and — since agent v0.155.0 — switch the whole machine off once per power-on, holding the hub's silence alarms for up to 14 days (R-998; mitigations and residual risk: `14-host-power-off.md` §5) |
|
|
| **added (R-1007):** controller → an app's own job queue | holds — nothing new | the `after_unclean_boot` commands run inside the app's container through the docker socket the controller already holds, and read the app's database with a read-only session: no key into the household's account, nothing a compromised controller could not already do (exec in any app, read its database). The household's lost-file names stay on the box (`app.yaml`, the app page); the operator event carries counts only, and a failing command's output is never logged (security review of controller `8d862e4`, fixed in `3b85a0c`) |
|
|
| **added:** agent ↔ root on the host | holds by the sudoers | every root verb is one regex-anchored line or a root wrapper that re-checks its input (`03` §3); agent v0.156.1 adds one delete: `felhom-vzdump-partial-clean` removes only a DEAD half-written vzdump of the named guest on a `dir` storage (no vzdump on the host, nothing holding it open) — a compromised agent gains the deletion of a leftover that is not a backup |
|
|
| agent ↔ hub | holds for destructive ops | signed jobs verified in the documented order; unsigned spec has no size bounds (R-984) |
|
|
| controller ↔ hub | **did not hold until 2026-10-12** | the global key was public (R-950, rotated) and three routes needed no key (R-951, fixed); per-box keys are bound to their box (R-955) |
|
|
| box ↔ PBS | holds as written | ciphertext only; the zero-knowledge wording is stronger than a live box allows (R-985) |
|
|
| guest ↔ Proxmox host | holds | the guest has no Proxmox credentials (measured) |
|
|
| hub ↔ Cloudflare API | holds for the zone check | tokens are stored unsealed in the hub database (R-975); R-137 open |
|
|
| **added:** app ↔ app (shared Docker daemon) | partly | separate containers and networks; any docker-socket app is root in the guest (R-981) |
|
|
| **added:** app ↔ controller | partly | apps share a network with the controller; dashboard routes still need a session |
|
|
| **added:** LAN ↔ box | partly | root password SSH on the host from the LAN (R-982) |
|
|
| **added:** hub ↔ box (inbound) | **does not hold** | `01` §4 says the box exposes no control endpoint; a hub-key route can replace the box's settings (R-970) |
|
|
| **added:** catalog ↔ every box | **design gap** | a catalog push reaches every box with no check on the box (R-971) |
|
|
|
|
## 6. What we accept (do not re-report)
|
|
|
|
- **Cloudflare can read tunnel traffic** — R-904, `09` §3 decision 184.
|
|
- **Signing keys on DooPlex with no passphrase until the first sale** — R-924, `04` §3.1 (operator ruling 1).
|
|
- **Public source** — the operator's decision (R-925).
|
|
- **A pasted Cloudflare token is checked for its zone only; the recipe guards the write side** — `09` §3 decision 226.
|
|
- **Per-name family sign-in lock** — `01` §5 (a stranger can lock one member out for 10 minutes; same shape as R-775).
|