feat: provision Alpine LXC 136 running Atheos, wired to Gitea #1

Merged
hermes merged 1 commits from feat/atheos-lxc-provision into master 2026-09-29 15:50:55 +10:00
Owner

Summary

Provisions LXC 136 on scotty: an Alpine 3.23 container running Atheos (a lightweight PHP
browser editor with real vim keybindings) served by nginx + php-fpm, with armistace/blog cloned
into its workspace and ready to edit. LAN-only. Built with the AI-DLC workflow — the full
process trail (requirements → design → units → audit) is in aidlc-docs/.

Deliberately not VS Code / code-server / Coder, and deliberately not a build environment:
the Pelican build stays in the existing pipeline, so there is intentionally no Python here.

Design decisions

# Decision Why
D1 No git identity configured by the agent The operator owns the armistace identity. Git refuses the first commit until they set it — deliberate, per requirement FR-11.
D2 Atheos pinned to commit f90cd18 (build 611) + verified tarball sha256 Atheos has no releases; a moving branch isn't reproducible. update-atheos.sh is the deliberate escape hatch.
D3 nginx :80, web root /var/www/atheos, php-fpm over unix socket Smallest network surface.
D6 Per-repository deploy key, not a PAT A Gitea PAT's scopes are route-groups matched against the whole account (write:repository reaches every repo the account can write). A deploy key reaches exactly one.

Verification — 40 checks, 0 failures, on the live guest

AC Result
AC-2 Atheos served over HTTP 200; nginx/php-fpm socket paths byte-match
AC-3 20 posts + themes/cleanblog; 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 by an actual container restart; marker came back byte-identical
AC-8 No python/pelican/pip/make and no DB engine — scope boundary enforced, not just intended
FR-11 git identity empty; deploy key mode 600, owner = service user

Traps found and documented

Each of these cost real debugging time and is now in the RUNBOOK:

  1. The Gitea SSH user is gitea, not git (app.ini SSH_USER=gitea). Using git makes sshd
    answer Invalid user git and reject before reading authorized_keys — indistinguishable
    from a bad key, and it sends you hunting for a problem that doesn't exist.
  2. The Gitea SSH endpoint is the LAN address — public :22 is not forwarded (only :443).
  3. uptime and free report the PVE host inside an LXC without lxcfs, so a container that
    restarted seconds ago can claim days of uptime.

Test plan

  • scripts/build-and-test.sh on the guest → 40 passed, 0 failed
  • Container restarted; services returned; clone + edits intact
  • workspace/blog working tree clean (test markers removed)
  • No credential, private key, or identity committed (.gitignore covers keys)
## Summary Provisions **LXC 136 on scotty**: an Alpine 3.23 container running **Atheos** (a lightweight PHP browser editor with real vim keybindings) served by nginx + php-fpm, with `armistace/blog` cloned into its workspace and ready to edit. LAN-only. Built with the **AI-DLC** workflow — the full process trail (requirements → design → units → audit) is in `aidlc-docs/`. Deliberately **not** VS Code / code-server / Coder, and deliberately **not a build environment**: the Pelican build stays in the existing pipeline, so there is intentionally no Python here. ## Design decisions | # | Decision | Why | |---|---|---| | D1 | **No git identity configured by the agent** | The operator owns the `armistace` identity. Git refuses the first commit until they set it — deliberate, per requirement FR-11. | | D2 | **Atheos pinned to commit `f90cd18`** (build 611) + verified tarball sha256 | Atheos has no releases; a moving branch isn't reproducible. `update-atheos.sh` is the deliberate escape hatch. | | D3 | nginx `:80`, web root `/var/www/atheos`, php-fpm over **unix socket** | Smallest network surface. | | D6 | **Per-repository deploy key**, not a PAT | A Gitea PAT's scopes are route-groups matched against the whole account (`write:repository` reaches **every** repo the account can write). A deploy key reaches exactly one. | ## Verification — 40 checks, 0 failures, on the live guest | AC | Result | |---|---| | AC-2 | Atheos served over HTTP **200**; nginx/php-fpm socket paths byte-match | | AC-3 | 20 posts + `themes/cleanblog`; 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 by an actual container restart; marker came back byte-identical | | AC-8 | **No** python/pelican/pip/make and no DB engine — scope boundary enforced, not just intended | | FR-11 | git identity empty; deploy key mode `600`, owner = service user | ## Traps found and documented Each of these cost real debugging time and is now in the RUNBOOK: 1. **The Gitea SSH user is `gitea`, not `git`** (`app.ini SSH_USER=gitea`). Using `git` makes sshd answer `Invalid user git` and reject **before reading `authorized_keys`** — indistinguishable from a bad key, and it sends you hunting for a problem that doesn't exist. 2. **The Gitea SSH endpoint is the LAN address** — public `:22` is not forwarded (only `:443`). 3. **`uptime` and `free` report the PVE host** inside an LXC without lxcfs, so a container that restarted seconds ago can claim days of uptime. ## Test plan - [x] `scripts/build-and-test.sh` on the guest → **40 passed, 0 failed** - [x] Container restarted; services returned; clone + edits intact - [x] `workspace/blog` working tree clean (test markers removed) - [x] No credential, private key, or identity committed (`.gitignore` covers keys)
Author
Owner

Response to independent review

Review round 1: code ✅ / security ✅ / infra ✅ Approved with two Minor/Needs-Change items.
Both adopted — neither blindly.

🔴 / 🟠 Adopted

1. provision.sh left the nginx vhost as a manual "see README" step. ✅ Fixed — a valid
catch: it broke this project's own repeatability requirement (FR-9), so a rebuild from scratch would
have served the distro default page. The script now writes the vhost itself, removes
/etc/nginx/http.d/default.conf, and then asserts the socket contract
(nginx fastcgi_pass == php-fpm pool listen) before Atheos is installed — so a 502 can only ever
mean an nginx/php-fpm problem and can never be mistaken for an Atheos one. Verified: contract MATCH.

2. free/uptime reported the PVE host (a container restarted seconds earlier claimed "up 2
days"
). ✅ Fixed — but not with the suggested fix. The review proposed apk add lxcfs in the
guest. That is not the fix: lxcfs is a host-provided FUSE filesystem, not a guest package, and
measurement shows it is already working correctly:

/proc/meminfo → MemTotal: 1048576 kB   ← our 1024 MB limit
/proc/uptime  → container seconds
/proc/cpuinfo → 2 processors

The real cause: on Alpine free/uptime are busybox applets reading via the sysinfo()
syscall
, which lxcfs cannot intercept. procps-ng ships binaries that read the virtualised /proc
files. Measured on the guest:

Tool Reports
procps-ng free 1024 MB ✅
busybox free 15701 MB ❌ host
procps-ng uptime up 2 min ✅
busybox uptime up 2 days ❌ host

So provision.sh now installs procps-ng; build-and-test.sh reports container-accurate figures.

📝 Noted, deliberately not actioned

  • allow_url_fopen=On — already recorded as an accepted risk, required by the Atheos plugin
    marketplace, and flagged again in docs/security-notes.md §2 (S-14) as the first thing to turn off
    if this is ever exposed. No change.
  • chmod 775 / 664 breadth — intentional and load-bearing: the installer requires exactly those
    writable paths (interface I-2), and nginx must read what the php-fpm user writes. Tightening further
    would break the install. Recorded in docs/security-notes.md.
  • healthz endpoint — reasonable, out of scope for a LAN PoC with no monitoring (R-3/S-12 record
    the same gap). Logged as a follow-up rather than merged scope creep.

Verification after the fixes

scripts/build-and-test.sh on the live guest: 40 passed, 0 failed. Resource line now reads
18MB used of 1024MB, up 2 min.

Independent review round 2 (incremental diff): code ✅ Approved / security ✅ Approved / infra ✅
Approved
— "Ready for merge."

## Response to independent review Review round 1: **code ✅ / security ✅ / infra ✅ Approved** with two Minor/Needs-Change items. Both adopted — neither blindly. ### 🔴 / 🟠 Adopted **1. `provision.sh` left the nginx vhost as a manual "see README" step.** ✅ **Fixed** — a valid catch: it broke this project's own repeatability requirement (FR-9), so a rebuild from scratch would have served the distro default page. The script now writes the vhost itself, removes `/etc/nginx/http.d/default.conf`, and then **asserts the socket contract** (`nginx fastcgi_pass` == `php-fpm pool listen`) *before* Atheos is installed — so a 502 can only ever mean an nginx/php-fpm problem and can never be mistaken for an Atheos one. Verified: contract MATCH. **2. `free`/`uptime` reported the PVE host** (a container restarted seconds earlier claimed *"up 2 days"*). ✅ **Fixed — but not with the suggested fix.** The review proposed `apk add lxcfs` in the guest. That is **not** the fix: lxcfs is a host-provided FUSE filesystem, not a guest package, and measurement shows it is already working correctly: ``` /proc/meminfo → MemTotal: 1048576 kB ← our 1024 MB limit /proc/uptime → container seconds /proc/cpuinfo → 2 processors ``` The real cause: on Alpine `free`/`uptime` are **busybox** applets reading via the **`sysinfo()` syscall**, which lxcfs cannot intercept. `procps-ng` ships binaries that read the virtualised `/proc` files. Measured on the guest: | Tool | Reports | | |---|---|---| | `procps-ng` `free` | `1024 MB` | ✅ | | busybox `free` | `15701 MB` | ❌ host | | `procps-ng` `uptime` | `up 2 min` | ✅ | | busybox `uptime` | `up 2 days` | ❌ host | So `provision.sh` now installs `procps-ng`; `build-and-test.sh` reports container-accurate figures. ### 📝 Noted, deliberately not actioned - **`allow_url_fopen=On`** — already recorded as an accepted risk, required by the Atheos plugin marketplace, and flagged again in `docs/security-notes.md` §2 (S-14) as the first thing to turn off if this is ever exposed. No change. - **`chmod 775` / `664` breadth** — intentional and load-bearing: the installer requires exactly those writable paths (interface I-2), and nginx must read what the php-fpm user writes. Tightening further would break the install. Recorded in `docs/security-notes.md`. - **`healthz` endpoint** — reasonable, out of scope for a LAN PoC with no monitoring (R-3/S-12 record the same gap). Logged as a follow-up rather than merged scope creep. ### Verification after the fixes `scripts/build-and-test.sh` on the live guest: **40 passed, 0 failed**. Resource line now reads `18MB used of 1024MB, up 2 min`. Independent review round 2 (incremental diff): **code ✅ Approved / security ✅ Approved / infra ✅ Approved** — "Ready for merge."
Author
Owner

Final Verdict

Review round 1 raised two items; both were addressed (one with a corrected diagnosis — the
suggested apk add lxcfs fix was wrong; the real cause was busybox using sysinfo(), and
procps-ng is the fix). Everything else was an explicit, documented accepted risk.

Independent review round 2: all three domains Approved. Verified on the live guest at
40 checks / 0 failures, including a real container restart for data persistence.

Merging.

## Final Verdict Review round 1 raised two items; both were addressed (one with a *corrected* diagnosis — the suggested `apk add lxcfs` fix was wrong; the real cause was busybox using `sysinfo()`, and `procps-ng` is the fix). Everything else was an explicit, documented accepted risk. Independent review round 2: all three domains Approved. Verified on the live guest at **40 checks / 0 failures**, including a real container restart for data persistence. Merging.
hermes added 1 commit 2026-09-29 15:50:49 +10:00
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.
hermes force-pushed feat/atheos-lxc-provision from 8f07c377c0 to a6fb61d0ea 2026-09-29 15:50:50 +10:00 Compare
hermes merged commit e038f758ac into master 2026-09-29 15:50:55 +10:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: hermes/atheos-blog-lxc#1