architecture 13: the threat model (draft) from the security review
gates / gates (push) Successful in 6m29s
gates / gates (push) Successful in 6m29s
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# 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 | 37 local-API routes | 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) and void the recovery code (R-978) |
|
||||
| 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).
|
||||
Reference in New Issue
Block a user