Files
atheos-blog-lxc/aidlc-docs/design-artifacts/infrastructure-design.md
hermes a6fb61d0ea feat: provision Alpine LXC 136 running Atheos, wired to Gitea
Provisions a LAN-only browser code editor (Atheos) in an Alpine 3.23 LXC on the
scotty PVE node, with armistace/blog cloned and ready to edit. Built via the
AI-DLC workflow; the full process trail is in aidlc-docs/.

Deliverables
- scripts/provision.sh          rebuild the guest from scratch (re-runnable)
- scripts/update-atheos.sh      move Atheos to latest upstream and RE-PIN
- scripts/build-and-test.sh     captures evidence for every acceptance criterion
- files/atheos.conf             nginx vhost (socket path byte-matches the FPM pool)
- files/ssh-config              ~atheos/.ssh/config deploy-key alias
- RUNBOOK.md                    operational guide (START AT STEP 1)
- docs/security-notes.md        credential model + wider-web hardening TODOs
- docs/resiliency-notes.md      what a non-PoC deployment would need

Design decisions
- D1 no git identity is configured by the agent; the operator sets it. Git will
  refuse the first commit until then, which is deliberate (requirement FR-11).
- D2 Atheos pinned to commit f90cd18 (build 611) with a verified tarball sha256,
  plus update-atheos.sh as the deliberate escape hatch.
- D3 nginx :80, web root /var/www/atheos, php-fpm over a unix socket.
- D6 git auth is a per-repository DEPLOY KEY scoped to armistace/blog, not a
  personal access token (a PAT's write:repository scope reaches every repo the
  account can write).

Verified on the live guest (40 checks, 0 failures)
- AC-2  Atheos served over HTTP 200 by nginx+php-fpm; socket paths byte-match
- AC-3  20 posts + themes/cleanblog present; owned by the php-fpm user
- AC-4  all five writable paths writable as the service user
- AC-5  remote targets armistace/blog; deploy key authenticates over SSH
- AC-6  survival proven with a real container restart, marker byte-identical
- AC-8  no python/pelican/pip/make and no DB engine - scope boundary enforced
- FR-11 git identity is empty; deploy key mode 600 owned by the service user

Traps documented in the RUNBOOK (each cost real debugging time)
- The Gitea SSH user is `gitea`, NOT `git` (app.ini SSH_USER=gitea). With `git`,
  sshd answers "Invalid user git" and rejects before reading authorized_keys,
  which is indistinguishable from a bad key.
- The Gitea SSH endpoint is the LAN address (:22 is not forwarded publicly).
- `uptime` and `free` report the PVE HOST inside an LXC without lxcfs, so a
  container that just restarted can claim days of uptime.
2026-09-29 15:50:34 +10:00

151 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Infrastructure Design — atheos-blog-lxc
**Stage**: CONSTRUCTION — Infrastructure Design
**Date**: 2026-09-29 · **Deploy target**: Proxmox LXC (Alpine 3.23) on node `scotty` (.69)
> **No open questions.** Every infrastructure decision was settled at the Requirements and Application
> Design gates (Q1–Q6, D1–D6). Re-asking them would be ceremony. This artifact maps the approved
> design onto concrete infrastructure, and records the *provenance* of each choice.
---
## 1. Guest specification (U1)
| Property | Value | Provenance / rationale |
|---|---|---|
| Node | **scotty** (192.168.178.69) | user Q3 |
| VMID | **136** | verified free — in-use on scotty: 104-107, 119, 125, 126, 133-135 |
| OS | **Alpine 3.23** (`alpine-3.23-default_20260116_amd64.tar.xz`) | user requirement ("alpine lxc") |
| Rootfs | **20 GB** | user requirement (point 5) |
| Storage | `scotty-local-lvm` (thin) | 771 GB free at design time; thin keeps 20 GB nominal, minimal actual |
| vCPU | 2 | idle PHP app; 1 risks slow composer-less PHP ops |
| RAM | 1024 MB | nginx + php-fpm is trivial; scotty has 12.1 GB available |
| Swap | 256 MB | PHP can spike on large files; cheap insurance |
| Privileged? | **unprivileged** | default-safe; nothing here needs device access |
| `onboot` | **1** | NFR-2 |
| Network | `eth0`, `bridge=vmbr0`, DHCP | matches all existing guests; LAN-only (FR-10) |
| DNS | router-provided | LAN-only |
| Timezone | `Australia/Brisbane` | NFR-5 |
| Features | **none required** | ⚠️ see note below |
### ⚠️ Note on LXC `features`
The observability project hit `status=226/NAMESPACE` because a container lacked **`nesting`**.
That was **systemd**. This guest is **OpenRC** on Alpine, which does not need `nesting`.
So: `features` left **unset**, and if a service fails to start we revisit `nesting` rather than
pre-emptively enabling a feature that widens the container's escape surface.
### Template provenance
The Alpine 3.23 template is **not yet present on scotty** (risk C-3). U1 therefore begins with
`pveam update` + `pveam download local alpine-3.23-default_...`, and the download is verified before
`pct create` runs.
---
## 2. Runtime stack (U2)
| Layer | Choice | Notes |
|---|---|---|
| HTTP | `nginx` (Alpine `main`) | listens `:80`, `root /var/www/atheos` |
| PHP | `php83-fpm` + `php83` CLI | Alpine 3.23's PHP is **8.3** — confirmed available (`php83` present in the v3.23 index) |
| Extensions | `php83-session`, `php83-zip`, `php83-openssl`, `php83-mbstring` | Atheos: ZIP/OPENSSL/MBSTRING for the plugin marketplace; `session` is not optional |
| PHP ini | `allow_url_fopen=On` | Atheos marketplace requirement (recorded as accepted risk; turn off if unused — S-14) |
| Socket | **unix socket** | nothing extra listens on the LAN; path resolved from the *installed* pool config, not assumed |
### The socket contract (interface I-1 — the 502 risk)
nginx's `fastcgi_pass` and the php-fpm pool's `listen` are two strings that must be **byte-identical**.
The design does not assume a path: U2 **reads the pool config after install** and writes nginx to
match, then asserts both sides agree. A mismatch produces a bare 502 with no other diagnostic.
---
## 3. Application layer (U3)
| Item | Value |
|---|---|
| Web root | `/var/www/atheos` |
| Source | `github.com/Atheos/Atheos`, **pinned to a resolved commit SHA** (D2) |
| Database | **none** — Atheos is file-backed |
| Writable paths | `config.php`, `data/`, `workspace/`, `plugins/`, `themes/` |
| Ownership | owned by the **php-fpm run user** (resolved from the pool) |
| Run user | resolved at U2; **U2/U3/U4 must agree on the same user** (cross-unit risk) |
---
## 4. VCS layer (U4)
| Item | Value | Provenance |
|---|---|---|
| `git` | Alpine package | user point 2 |
| `safe.directory` | workspace path | prevents *dubious ownership* (I-3) |
| Committer identity | **deliberately UNSET** | **D1 / Q1=B** — operator sets it first |
| Clone | `armistace/blog` branch `master` → `workspace/blog` | user point 3, Q6 |
| Remote (initial) | `https://git.aridgwayweb.com/armistace/blog.git` → **HTTPS** (public repo) | U4 phase 1 |
| Credential | **ed25519 deploy key**, per-repo, write-enabled | **D6** |
| Private key | `~<fpmuser>/.ssh/id_ed25519_blog`, mode `600`, owned by FPM user | D6 |
| SSH alias | `Host gitea-blog` + `IdentitiesOnly yes` | D6 — a stray push cannot hit another repo |
| Remote (final) | `gitea-blog:armistace/blog.git` | U4 **phase 2**, after operator installs the key |
---
## 5. Networking & exposure
- **LAN-only** (FR-10). nginx binds `:80` on the guest; nothing else is configured.
- **No NPM proxy host, no DNS record, no router forward** — deliberately not created.
- php-fpm on a unix socket ⇒ **only `:80` and `:22` listen**.
- `git.aridgwayweb.com` resolves to a **public IP** — the *clone* traverses the internet, but the
guest itself is not exposed. Noted because it makes Gitea-side scoping (D6) the meaningful control.
---
## 6. Operations & persistence (OpenRC, not systemd)
| Service | Enable command | Failure if missed |
|---|---|---|
| `nginx` | `rc-update add nginx default` | serves until reboot, then **AC-6 fails** |
| `php-fpm83` | `rc-update add php-fpm83 default` | same |
- Logs: `/var/log/nginx/`, php-fpm error log (path resolved at install)
- **Verification uses an actual reboot**, not a service-status check — a service can be "started"
and still not be enabled.
- Monitoring/alerting: **out of scope** (R-3, S-12). Observability stack is a separate project.
---
## 7. Deployment architecture
```
LAN 192.168.178.0/24
│
┌────────▼─────────┐
│ scotty (.69) │ PVE node
│ ┌────────────┐ │
│ │ LXC 136 │ │ Alpine 3.23 · 2 vCPU · 1 GB · 20 GB root
│ │ │ │
:80 ─────────┼──┤ nginx │ │ unprivileged · onboot=1
│ │ │ unix │ │
│ │ ▼ socket │ │
│ │ php-fpm83 │ │
│ │ │ │ │
│ │ ├─► /var/www/atheos{,/workspace/blog}
│ │ └─► git ─┼──┼──► gitea-blog:armistace/blog.git
│ └────────────┘ │ (deploy key, D6)
└──────────────────┘
│
internet (clone only)
▼
git.aridgwayweb.com (Gitea LXC 109)
```
---
## 8. Infrastructure decisions summary
| # | Decision | Why |
|---|---|---|
| I-1 | Unprivileged, no `features` | default-safe; `nesting` is a systemd need, not ours |
| I-2 | Thin-provisioned `scotty-local-lvm` | 20 GB nominal on 771 GB free; bounded blast radius |
| I-3 | php-fpm over **unix socket** | smallest network surface |
| I-4 | Same user for FPM, file ownership, and key | three-way identity agreement — the top cross-unit risk |
| I-5 | **No** monitoring/alerting | out of scope, recorded (R-3/S-12) |
| I-6 | Reboot-based verification | a "started" service is not an "enabled" one |