platform/docs/01-architecture-and-threat-model.md
Bart Van Geyt 9f819df4d9 Phase 0: architecture docs, ADRs, and repo scaffold
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>
2026-07-07 12:26:00 +02:00

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

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

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