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>
124 lines
6.7 KiB
Markdown
124 lines
6.7 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-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](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.
|