platform/docs/01-architecture-and-threat-model.md
Bart Van Geyt e5ff798dd4 Refine to nested customer -> site model
Adopt a two-level ownership model: a customer owns many sites, with
isolation/containers/DB/backup at the site level and grouping (one SFTP
login, recursive backup, billing, bulk delete) at the customer level.

- 03-naming-conventions: rewritten for customer/site ids and slug =
  <customer>-<site>; nested ZFS, per-customer SFTP chroot, deployments
  mirror the nesting.
- Propagated paths through docs 01, 04, 05, 06, 07 and deployments/README.
- ADR 0005 retitled/updated to per-site datasets nested under customer.
- architecture-plan.md: note pointing to doc 03 as authoritative on naming.

Clarifies subdomains: same-app subdomains are aliases on one site; a
separate-app subdomain is its own isolated site under the same customer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 12:37:39 +02:00

128 lines
6.9 KiB
Markdown

# 01 — Architecture & Threat Model
## 1. Purpose
Rebuild the hosting business as an **isolation-first, backup-clean,
repeatably-provisioned** platform on a single bare-metal host. Serves a mix of
WordPress, custom PHP, static HTML, and redirect-only sites.
## 2. High-level architecture
```
Internet
(80/443, SFTP, admin SSH)
┌────────▼────────┐
│ Traefik │ edge: TLS termination, ACME,
│ (edge router) │ dynamic label-based routing
└───┬─────────┬───┘
│ │ shared "proxy" network
┌───────────────┘ └───────────────┐
│ per-site network (acme-shop_net) │ per-site network (bar-www_net)
┌────▼─────┐ ┌──────────┐ ┌─────▼────┐ ┌──────────┐
│ nginx │──▶│ php-fpm │ │ nginx │──▶│ php-fpm │
│acme/shop │ │acme/shop │ │ bar/www │ │ bar/www │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ web root vol │ │ web root vol │
│ └──────────┐ ┌──────────┘ │
│ ▼ ▼ │
│ ┌─────────────────┐ │
│ │ shared MariaDB │ per-site DB + │
│ │ (platform net) │ least-priv user │
│ └─────────────────┘ │
▼ ▼
tank/customers/acme/shop/web tank/customers/bar/www/web
```
The isolation unit is the **site** (`<customer>/<site>`). A customer may own
several sites, each fully isolated from the others but grouped under the customer
for SFTP/backup/billing (see [03](03-naming-conventions.md)).
Platform services (not shown per-site): Forgejo + registry, Prometheus / Grafana
/ Loki, Uptime-Kuma. All run as their own compose projects on the host.
### Request path
1. DNS points the customer domain at the host.
2. Traefik terminates TLS (Let's Encrypt) and routes by Host rule (from compose
labels) onto the customer's network.
3. For PHP profiles: nginx serves static assets and proxies `.php` over FastCGI
to that site's php-fpm. For static/redirect: Traefik or a tiny nginx answers
directly.
4. php-fpm talks to shared MariaDB over the platform network using the site's
own database + least-privilege credentials.
### Why Traefik *and* nginx (not either/or)
Traefik does not speak FastCGI, so it cannot talk to php-fpm directly. Traefik
owns the edge (TLS, routing, ACME); a small per-site nginx bridges HTTP→FastCGI.
This is deliberate, not redundancy.
## 3. Isolation boundaries
| Boundary | Mechanism | Protects against |
|----------|-----------|------------------|
| Process/filesystem | Separate containers per site | One site reading another's files/processes |
| Network (east-west) | Per-site Docker bridge; only Traefik bridges to `proxy` | Site A reaching Site B's containers (even same customer) |
| Data at rest | ZFS dataset per site (nested under customer), bind-mounted web root | Cross-site data access; enables clean per-site restore |
| Database | Per-site DB + least-privilege user on shared instance | Site A reading Site B's tables |
| Privilege | php-fpm non-root; read-only rootfs where possible; writable web root only | Privilege escalation within a container |
| Egress | Firewall egress filtering (esp. SMTP) | Hacked site sending spam / exfiltration |
## 4. Threat model
### 4.1 Assets
- Customer website files and databases.
- The host itself (kernel, Docker daemon, ZFS pool).
- Platform credentials (DB root, Traefik/ACME, Forgejo, SSH).
- Backups (onsite snapshots + offsite copies).
### 4.2 Primary threat actors & scenarios
1. **Compromised WordPress/PHP app** (most likely). Attacker gets code execution
inside one site's php-fpm container.
2. **Malicious/abusive tenant.**
3. **Credential theft** (leaked DB or SFTP creds).
4. **External network attacker** probing exposed ports.
### 4.3 What each scenario can and cannot do
**Compromised app container (S1):**
- ✅ Contained to: that site's files, that site's DB (its creds only), its own
network namespace.
- ❌ Blocked from: other customers' files (separate datasets/containers), other
DBs (least-privilege user), other customers' networks (no route), spamming
(egress SMTP filtered).
- ⚠️ **Residual risk:** containers share the host kernel — a kernel or Docker
escape breaks isolation. Mitigate with: patched host, non-root FPM, dropped
capabilities, no `--privileged`, read-only rootfs, seccomp defaults; later
gVisor and/or Falco runtime detection.
**Malicious tenant (S2):** same containment as S1, plus resource limits
(CPU/memory per compose project) to prevent noisy-neighbour DoS.
**Credential theft (S3):** blast radius limited to that one site because every
site has its own DB user and its own SFTP chroot. Rotate via CLI.
**Network attacker (S4):** only 80/443, SFTP, and admin SSH are exposed;
everything else denied by nftables. Admin SSH key-only + hardened.
### 4.4 Explicit non-goals / accepted risks
- **Containers are not a hardened sandbox.** We accept shared-kernel risk on a
single host and mitigate in depth rather than claiming VM-grade isolation.
- **Backups are crash-consistent, not transactionally atomic** across the
web-file and DB streams (see [06](06-backup-and-dr.md)). Acceptable for
PHP/WordPress workloads.
- No HA / multi-host failover in the initial design (single host by choice).
## 5. Security controls checklist (implemented across phases)
- [ ] Host: nftables default-deny inbound, egress SMTP filtered, SSH key-only.
- [ ] Docker: no `--privileged`, drop capabilities, read-only rootfs where
possible, per-project resource limits, userns considered.
- [ ] Per-customer network isolation; Traefik the only cross-network bridge.
- [ ] php-fpm runs as non-root; web root the only writable mount.
- [ ] Per-site DB users with least privilege; no shared DB accounts.
- [ ] SFTP chrooted per customer.
- [ ] Images scanned (Trivy) and secrets scanned (gitleaks) in CI.
- [ ] Automatic TLS via Traefik/ACME; HSTS.
- [ ] (Later) Falco runtime anomaly detection; gVisor for higher-risk tenants.
See [ADRs](adr/) for the rationale behind each major choice.