Addresses the two valid items from the independent review (code/security/infra all
Approved; these were Minor/Needs-Change items).
1. provision.sh left the nginx vhost as a manual "see README" step, which broke the
project's own repeatability requirement (FR-9): a rebuild from scratch would have
served the distro default page instead of Atheos. The vhost is now written by the
script itself, the distro default.conf is removed, and the script 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, never be mistaken for an Atheos one.
2. `free` and `uptime` reported the PVE HOST inside the container (a container that had
restarted seconds earlier claimed "up 2 days"), which is misleading in monitoring and
in the test output.
The review suggested `apk add lxcfs` in the guest. That is NOT the fix - lxcfs is a
host-provided FUSE filesystem, not a guest package, and measurement showed it is
already working correctly here:
/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 that read via the
sysinfo() SYSCALL, which lxcfs cannot intercept. procps-ng ships binaries that read
the lxcfs-virtualised /proc files instead. Measured on the guest:
procps-ng free -> 1024 MB (correct)
busybox free -> 15701 MB (host)
procps-ng uptime -> up 2 min (correct)
busybox uptime -> up 2 days (host)
So provision.sh now installs procps-ng, and build-and-test.sh reports container-
accurate figures (detecting it via apk rather than a binary-existence probe, since
the busybox applets are plain files too).
Verified after the change: build-and-test 40 passed / 0 failed; socket contract MATCH;
resource line now reads "18MB used of 1024MB, up 2 min".
299 lines
12 KiB
Markdown
299 lines
12 KiB
Markdown
# RUNBOOK — atheos-blog-lxc
|
|
|
|
Atheos browser editor for `armistace/blog`, running in an Alpine LXC on Proxmox.
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Guest** | LXC **136** on node `scotty` (192.168.178.69) |
|
|
| **IP** | **http://192.168.178.57/** |
|
|
| **OS** | Alpine 3.23.2 · unprivileged · 2 vCPU · 1 GB RAM · 20 GB root · `onboot=1` |
|
|
| **App** | Atheos IDE, pinned to commit `f90cd1806dbe0e25673a77e27390e8b1a240c3f6` (build 611) |
|
|
| **Repo** | `git.aridgwayweb.com/hermes/atheos-blog-lxc` (this provisioning repo) |
|
|
| **Workspace** | `/var/www/atheos/workspace/blog` → clone of `armistace/blog` |
|
|
| **Access** | LAN only. No NPM proxy host, no DNS record, no port-forward. |
|
|
|
|
---
|
|
|
|
## ⚠️ STEP 1 — DO THIS FIRST: set your git identity
|
|
|
|
**The agent deliberately did not configure a git identity** (design decision D1, requirement FR-11).
|
|
Git will **refuse to commit** until you set one. This is your first action after SSH-ing in.
|
|
|
|
```sh
|
|
ssh root@192.168.178.57
|
|
|
|
# become the user that runs PHP and owns the workspace
|
|
su -s /bin/sh atheos
|
|
|
|
# set YOUR identity (these values are placeholders - use your real ones)
|
|
git config --global user.name "Andrew Ridgway"
|
|
git config --global user.email "ar17787@gmail.com"
|
|
|
|
# verify
|
|
git config --global --get user.name
|
|
git config --global --get user.email
|
|
```
|
|
|
|
> Why you and not the agent: you own the `armistace` identity. Configuring it under your name
|
|
> programmatically would impersonate you and blur commit attribution.
|
|
|
|
The same identity must be set for the deploy key's user, which is the **same account** (`atheos`),
|
|
so the single `su` above covers both the editor's commits and anything you do by hand.
|
|
|
|
---
|
|
|
|
## STEP 2 — install the deploy key (Phase 2 prerequisite)
|
|
|
|
The guest already has a per-repo deploy keypair. Only the **public** half is shared:
|
|
|
|
```sh
|
|
# print it again if you need it
|
|
cat /home/atheos/.ssh/id_ed25519_blog.pub
|
|
```
|
|
|
|
Add it in Gitea:
|
|
|
|
1. Open `armistace/blog` → **Settings** → **Deploy Keys** → **Add Deploy Key**
|
|
2. Title: `atheos-blog-lxc`
|
|
3. Paste the public key
|
|
4. ✅ **TICK "Allow write access"** — required, otherwise push is rejected
|
|
|
|
Public key (as generated 2026-09-29):
|
|
|
|
```
|
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINj03Tep9pULTvVDCC3q1ZtocKTZSmDhPsB61Zic1SPk atheos-blog-lxc deploy key (armistace/blog)
|
|
```
|
|
|
|
Fingerprint to confirm you pasted the right one:
|
|
|
|
```
|
|
SHA256:qHvsjjSX79aPZRoNTRI4j+HH3hArAuINIRu9ewxx7t8 (ED25519)
|
|
```
|
|
|
|
---
|
|
|
|
## STEP 3 — switch the remote to SSH (Phase 2)
|
|
|
|
Once the key is installed, tell the agent (or run it yourself):
|
|
|
|
```sh
|
|
su -s /bin/sh atheos -c \
|
|
'git -C /var/www/atheos/workspace/blog remote set-url origin gitea-blog:armistace/blog.git'
|
|
|
|
# verify the transport actually authenticates
|
|
su -s /bin/sh atheos -c 'ssh -T gitea-blog' # expect a Gitea greeting, not "Permission denied"
|
|
su -s /bin/sh atheos -c 'git -C /var/www/atheos/workspace/blog fetch --dry-run'
|
|
```
|
|
|
|
### Why the remote uses `gitea-blog` and not a hostname
|
|
|
|
`gitea-blog` is an SSH config alias defined in `/home/atheos/.ssh/config`:
|
|
|
|
```
|
|
Host gitea-blog
|
|
HostName 192.168.178.155 # LAN address
|
|
User gitea # <-- NOT `git`! see the trap below
|
|
IdentityFile ~/.ssh/id_ed25519_blog
|
|
IdentitiesOnly yes
|
|
Port 22
|
|
```
|
|
|
|
> ### ⚠️ TRAP: the SSH user is `gitea`, not `git`
|
|
> Every GitHub/Gitea tutorial uses `git@host`. **This instance does not.** Its `app.ini` sets
|
|
> `SSH_USER = gitea`.
|
|
>
|
|
> If you use `User git`, sshd replies **`Invalid user git`** and rejects the connection
|
|
> **before it ever reads `authorized_keys`** — so the error looks exactly like a rejected key
|
|
> and sends you hunting for a problem that isn't there. Confirmed in the sshd log on LXC 109:
|
|
> ```
|
|
> Invalid user git from 192.168.178.57 port 39780
|
|
> Connection closed by invalid user git 192.168.178.57 [preauth]
|
|
> ```
|
|
> Correct user → `Hi there! You've successfully authenticated with the deploy key named atheos-blog-lxc`.
|
|
|
|
Two reasons for the alias:
|
|
|
|
1. **`IdentitiesOnly yes`** means no other key is ever offered — a stray `git push` cannot
|
|
authenticate with a different (possibly broader) identity.
|
|
2. **It's the LAN address because that's the only path that works.** Verified 2026-09-29:
|
|
|
|
| Endpoint | Result |
|
|
|---|---|
|
|
| `git.aridgwayweb.com:22` | **unreachable** — the public path does not forward :22 (only :443 via NPM) |
|
|
| `192.168.178.155:22` | **OPEN** — Gitea's `app.ini` sets `SSH_DOMAIN = 192.168.178.155`, `SSH_PORT = 22`, `START_SSH_SERVER = false` |
|
|
|
|
So Gitea serves SSH from the LXC's own `sshd` and advertises the LAN address. Since this guest is
|
|
LAN-only, LAN transport is also the correct choice — no internet round-trip.
|
|
|
|
---
|
|
|
|
## STEP 4 — complete the Atheos installer
|
|
|
|
Open **http://192.168.178.57/** in a browser. The Atheos first-run installer appears and creates
|
|
the **admin user** — this is yours to own; the agent seeded no accounts.
|
|
|
|
Verify afterwards:
|
|
|
|
- you can open the **Workspace** and see a `blog` project
|
|
- opening `src/content/*.md` shows your posts
|
|
- saving persists (the files are at `/var/www/atheos/workspace/blog`)
|
|
|
|
### Atheos git settings
|
|
|
|
Open Atheos → (user menu) → **Settings** to confirm the git integration. The `codegit` component
|
|
handles commit/push. If you see *"detected dubious ownership"*, the `safe.directory` entries are
|
|
missing — restore them with:
|
|
|
|
```sh
|
|
su -s /bin/sh atheos -c 'git config --global --add safe.directory /var/www/atheos/workspace'
|
|
su -s /bin/sh atheos -c 'git config --global --add safe.directory /var/www/atheos/workspace/blog'
|
|
```
|
|
|
|
---
|
|
|
|
## Day-to-day operations
|
|
|
|
### Edit and commit
|
|
|
|
Do everything in the browser at `http://192.168.178.57/`. Commit via Atheos's git panel, or by hand:
|
|
|
|
```sh
|
|
ssh root@192.168.178.57
|
|
su -s /bin/sh atheos
|
|
cd /var/www/atheos/workspace/blog
|
|
git status
|
|
git add -A && git commit -m "post: <what changed>"
|
|
git push
|
|
```
|
|
|
|
### The build is NOT here
|
|
|
|
This box **only writes markdown**. Building/publishing the Pelican site happens in the existing
|
|
pipeline elsewhere. There is deliberately **no Python, no pelican, no `make`** on this guest.
|
|
|
|
### Services (OpenRC — not systemd)
|
|
|
|
```sh
|
|
rc-service nginx status
|
|
rc-service php-fpm83 status
|
|
rc-update show | grep -E 'nginx|php-fpm83' # both must be in 'default'
|
|
```
|
|
|
|
### Logs
|
|
|
|
```sh
|
|
tail -f /var/log/nginx/atheos.error.log # PHP fatals surface here
|
|
tail -f /var/log/nginx/atheos.access.log
|
|
```
|
|
|
|
### Reboot / recovery
|
|
|
|
```sh
|
|
pct reboot 136 # from scotty, or
|
|
pct stop 136 && pct start 136
|
|
```
|
|
|
|
Services are enabled at the OpenRC `default` runlevel, so they return automatically
|
|
(verified — see `aidlc-docs` acceptance criteria AC-6).
|
|
|
|
### Rebuild from scratch
|
|
|
|
The whole guest is reproducible:
|
|
|
|
```sh
|
|
# on scotty (192.168.178.69), as root
|
|
pct destroy 136
|
|
sh scripts/provision.sh # from this repo, on the node
|
|
```
|
|
|
|
Nothing unique lives on the guest: the blog's source of truth is Gitea. Only **uncommitted edits**
|
|
would be lost — commit often.
|
|
|
|
### Update Atheos (and re-pin)
|
|
|
|
Atheos has no releases, so this install is pinned to a commit. To move to the latest upstream commit,
|
|
install it, and re-pin:
|
|
|
|
```sh
|
|
sh scripts/update-atheos.sh # on the guest; preserves workspace/, backs up, verifies, re-pins
|
|
```
|
|
|
|
---
|
|
|
|
## Security model (summary — full detail in `docs/security-notes.md`)
|
|
|
|
- **Git auth = a per-repository deploy key**, scoped to `armistace/blog`, write-enabled.
|
|
Not a PAT (whose `write:repository` scope reaches **every** repo the account can write).
|
|
- Private key: `/home/atheos/.ssh/id_ed25519_blog`, mode `600`, owned by the php-fpm user.
|
|
- The web app and the key share one unprivileged user, so a compromised Atheos **can** write `blog`.
|
|
That is the accepted floor — one repo, and (with branch protection) one branch.
|
|
- **LAN-only.** If this is ever exposed: keep branch protection on `blog`'s `master`, terminate TLS
|
|
at NPM, and put authentication *in front* of Atheos. See `docs/security-notes.md` §2 (S-1…S-14).
|
|
|
|
### Container resource reporting (a real trap — cost debugging time)
|
|
|
|
`uptime` and `free` on Alpine are **busybox** applets. Busybox reads via the **`sysinfo()` syscall**,
|
|
which lxcfs **cannot intercept** — so inside this container they report the **PVE host**, not the
|
|
container. A container that restarted seconds ago will cheerfully claim *"up 2 days"*.
|
|
|
|
lxcfs **is** working correctly here — the virtualised files are right:
|
|
|
|
```
|
|
/proc/meminfo → MemTotal: 1048576 kB ← our 1024 MB limit
|
|
/proc/uptime → 105 s ← container
|
|
/proc/cpuinfo → 2 processors ← container
|
|
```
|
|
|
|
The container is correct; **the tool is wrong**. Fix = `apk add procps-ng`, whose `/usr/bin/free`
|
|
and `/usr/bin/uptime` read those virtualised `/proc` files:
|
|
|
|
| Tool | Reports | |
|
|
|---|---|---|
|
|
| `procps-ng` `free` | `1024 MB` | ✅ correct |
|
|
| busybox `free` | `15701 MB` | ❌ host |
|
|
| `procps-ng` `uptime` | `up 2 min` | ✅ correct |
|
|
| busybox `uptime` | `up 2 days` | ❌ host |
|
|
|
|
> **Note:** `apk add lxcfs` inside the guest is **not** a fix — lxcfs is a host-provided FUSE
|
|
> filesystem, not a guest package. `provision.sh` installs `procps-ng` for this reason.
|
|
> (`df` is unaffected: it stats the filesystem directly, so it was always accurate.)
|
|
|
|
---
|
|
|
|
## Reaching the guest when SSH from your laptop fails
|
|
|
|
The guest has **root SSH enabled** with your fleet keys installed:
|
|
|
|
- `ssh-rsa ... aridgway@strategenics-25`
|
|
- `ssh-ed25519 ... oct2025`
|
|
|
|
If those keys aren't what you're using, add/push a key via the PVE node (which always works):
|
|
|
|
```sh
|
|
ssh root@192.168.178.69 "pct exec 136 -- sh -c 'echo \"<your-pubkey>\" >> /root/.ssh/authorized_keys'"
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause | Fix |
|
|
|---|---|---|
|
|
| **502 Bad Gateway** | nginx `fastcgi_pass` and the FPM pool `listen` disagree | `grep -E '^listen' /etc/php83/php-fpm.d/www.conf` and compare with the `fastcgi_pass` line in `/etc/nginx/http.d/atheos.conf`. Both must read `/run/php-fpm83/php-fpm.sock`. Then restart both services. |
|
|
| **"detected dubious ownership"** | `safe.directory` missing or the repo is owned by a different user | Re-add the two `safe.directory` entries (STEP 4) |
|
|
| **git commit refuses: "please tell me who you are"** | identity not set — **expected on a fresh build** | STEP 1 |
|
|
| **git push: Permission denied (publickey)** | deploy key not installed, **or the SSH user is wrong**, or "Allow write access" not ticked | Check all three. Especially: `grep User /home/atheos/.ssh/config` must say **`gitea`**, not `git` (see STEP 3 trap). On the Gitea host, confirm with `journalctl -u ssh \| tail` — `Invalid user git` means the username is wrong, not the key. |
|
|
| **push times out** | remote still pointing at `git.aridgwayweb.com` (:22 not forwarded) | STEP 3 — use the `gitea-blog` alias |
|
|
| **Atheos shows a blank page / 500** | PHP fatal | `tail -50 /var/log/nginx/atheos.error.log` |
|
|
| **Atheos installer says a path isn't writable** | ownership broken | `chown -R atheos:atheos /var/www/atheos` |
|
|
| **`free` / `uptime` show the PVE host's memory or days of uptime** | on Alpine these are **busybox** applets using the `sysinfo()` **syscall**, which lxcfs cannot intercept | `apk add procps-ng` — its `/usr/bin/free` and `/usr/bin/uptime` read the lxcfs-virtualised `/proc` files. Already installed by `provision.sh`; this bites if you rebuild by hand. |
|
|
| **PHP session errors** | rootfs full | `df -h /` (the rootfs is 20 GB and the app is tiny, so this indicates something else) |
|
|
|
|
---
|
|
|
|
## Change log
|
|
|
|
| Date | Change |
|
|
|---|---|
|
|
| 2026-09-29 | Initial build: LXC 136 on scotty, Alpine 3.23.2, nginx + php83-fpm (socket), Atheos pinned at `f90cd18` (build 611), `armistace/blog` cloned over HTTPS, per-repo deploy key generated. LAN-only. |
|