# 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** (`/`). 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.