Establish the design foundation for the heleosv2 multi-tenant hosting platform before any implementation code: - Monorepo skeleton: docs/, platform-infra/, site-templates/, deployments/, control-panel/ with orientation READMEs. - docs/: roadmap index, architecture + threat model, naming conventions, site profiles, provisioning workflow, backup & DR runbook, repo/GitOps layout, and the approved architecture plan. - docs/adr/: 9 ADRs recording the rationale for single-host Compose, Traefik edge, nginx+fpm split, shared MariaDB, ZFS-per-customer, decoupled backup streams, Forgejo, CLI-first, and SFTP-only. - Secrets hygiene: .gitignore (only *.enc.* committed) and .gitattributes (LF for scripts/Dockerfiles/YAML run on the Linux host). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.7 KiB
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-customer network A │ per-customer network B
┌────▼─────┐ ┌──────────┐ ┌─────▼────┐ ┌──────────┐
│ nginx │──▶│ php-fpm │ │ nginx │──▶│ php-fpm │
│ (site A) │ │ (site A) │ │ (site B) │ │ (site B) │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ web root vol │ │ web root vol │
│ └──────────┐ ┌──────────┘ │
│ ▼ ▼ │
│ ┌─────────────────┐ │
│ │ shared MariaDB │ per-site DB + │
│ │ (platform net) │ least-priv user │
│ └─────────────────┘ │
▼ ▼
ZFS tank/customers/A/web ZFS tank/customers/B/web
Platform services (not shown per-customer): Forgejo + registry, Prometheus / Grafana / Loki, Uptime-Kuma. All run as their own compose projects on the host.
Request path
- DNS points the customer domain at the host.
- Traefik terminates TLS (Let's Encrypt) and routes by Host rule (from compose labels) onto the customer's network.
- For PHP profiles: nginx serves static assets and proxies
.phpover FastCGI to that site's php-fpm. For static/redirect: Traefik or a tiny nginx answers directly. - 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-customer Docker bridge; only Traefik bridges to proxy |
Site A reaching Site B's containers |
| Data at rest | ZFS dataset per customer, bind-mounted web root | Cross-customer data access; enables clean 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
- Compromised WordPress/PHP app (most likely). Attacker gets code execution inside one site's php-fpm container.
- Malicious/abusive tenant.
- Credential theft (leaked DB or SFTP creds).
- 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). 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 for the rationale behind each major choice.