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>
128 lines
6.9 KiB
Markdown
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.
|