Files
felhom.eu/documentation/architecture/13-threat-model.md
T
2026-10-12 17:18:08 +02:00

6.0 KiB

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).