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.
This commit is contained in:
2026-09-29 15:50:34 +10:00
parent 27272e6f6f
commit a6fb61d0ea
26 changed files with 2420 additions and 1 deletions
+43
View File
@@ -0,0 +1,43 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /var/www/atheos;
index index.php index.html;
access_log /var/log/nginx/atheos.access.log;
error_log /var/log/nginx/atheos.error.log;
# --- PHP -----------------------------------------------------------------
# The socket path here MUST byte-match the pool's `listen =` in
# /etc/php83/php-fpm.d/www.conf
# (interface I-1 — a mismatch yields a bare 502 with no other diagnostic).
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php-fpm83/php-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location / {
try_files $uri $uri/ =404;
}
# --- hardening -----------------------------------------------------------
location ~ /\.git { deny all; }
location ~ /\.ssh { deny all; }
location ~ /\. { deny all; }
location ~ ^/data/ { deny all; }
location ~ ^/(config\.php|common\.php|controller\.php|traits/) { deny all; }
location ~* \.(md|py|sh|log|conf|ini|sql|bak|orig)$ { deny all; }
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
client_max_body_size 64m;
}
+25
View File
@@ -0,0 +1,25 @@
# ~atheos/.ssh/config — install to /home/atheos/.ssh/config (mode 600, owner atheos)
#
# gitea-blog: per-repository deploy key for armistace/blog (design decision D6).
#
# !! THE USER IS `gitea`, NOT `git` !!
# This instance's app.ini sets SSH_USER = gitea.
# With `git` the sshd on that host answers "Invalid user git" and rejects the
# connection BEFORE reading authorized_keys - which looks identical to a
# rejected key. Verified in the sshd log on LXC 109, 2026-09-29.
#
# Why the LAN address rather than git.aridgwayweb.com:
# - app.ini sets SSH_DOMAIN = 192.168.178.155 / SSH_PORT = 22
# with START_SSH_SERVER = false, i.e. SSH is served by the Gitea LXC's own sshd.
# - the public path does NOT forward :22 (only :443, via NPM). Verified:
# git.aridgwayweb.com:22 -> unreachable
# 192.168.178.155:22 -> OPEN
# - this guest is LAN-only, so LAN transport is correct and avoids an internet round-trip.
#
# IdentitiesOnly yes ensures no other key is ever offered to the server.
Host gitea-blog
HostName 192.168.178.155
User gitea
IdentityFile ~/.ssh/id_ed25519_blog
IdentitiesOnly yes
Port 22