# Security Notes — atheos-blog-lxc **Status**: security-baseline extension **opted out** (LAN-only PoC) per Andrew's standing preference. **Purpose**: preserve the hardening trail so that *if this ever moves to the wider web*, the work is already scoped, and nobody has to re-derive it. Every item names its code touch-point. **Reviewed**: 2026-09-29. Design decision **D6** (see `application-design.md`). --- ## 1. The credential model (the load-bearing decision) **Chosen: a per-repository Gitea _deploy key_, scoped to `armistace/blog`, write-enabled.** ### Why not the alternatives | Option | Scope if stolen | Verdict | |---|---|---| | **Deploy key** (chosen) | `armistace/blog` **only** | ✅ per-repo scoping, explicit `read_only` flag | | `armistace` personal access token | **every repo armistace can write** (scopes are route-groups like `write:repository`, matched against user access — *not* per-repo) | ❌ account-wide blast radius | | `armistace` personal SSH key | whole account, incl. web/API | ❌ | | Dedicated "Atheos" Gitea user | whatever it owns/grants — **no reduction** for `blog` | ❌ adds a credential + muddies attribution | **Verified, not assumed** (2026-09-29, live instance 1.27.3): - API schema: `AccessToken.scopes` = `["read:repository","write:repository",…]`; `DeployKey` carries `read_only: bool` and is bound to one repository. Deploy-key endpoints `/repos/{owner}/{repo}/keys` exist and are **per-repo**. - The instance's own token inventory shows the risk concretely: `armistace` holds 8 access tokens, one with scope **`all`**. > **Threat model this addresses.** Atheos is itself the untrusted component: it is a file editor > with shell-exec reach (`codegit` invokes `git`; Atheos uses `exec`/`system`) and it is reachable by > anyone on the LAN. The goal is not to make compromise impossible — it is to make the **blast radius > one repository** when it happens. ### Implementation (code touch-points) | Item | Where | Detail | |---|---|---| | Keypair generated **on the guest**, never leaves it | `scripts/provision.sh`, U4 | `ssh-keygen -t ed25519 -f ~//.ssh/id_ed25519_blog -N ''` | | Key owned **only** by the php-fpm run user, mode `600` | U4 | not readable by nginx or other users | | `IdentitiesOnly yes` + a named host alias | `~//.ssh/config` | `Host gitea-blog` so a stray push cannot target another repo | | Remote uses the alias | U4 | `gitea-blog:armistace/blog.git` | | **Public** half handed to the operator | via Matrix | operator adds it: repo → Settings → Deploy Keys → **Allow write access** | ⚠️ **Honest limitation.** For push to work, the deploy key must be readable by the web-app user, so a full Atheos compromise *can* use it to write `blog`. Unavoidable for any push-capable editor. The deploy key makes that the **floor** (one repo, one protected branch), not the **ceiling** (8 repos). --- ## 2. If this is ever exposed beyond the LAN | # | Item | Touch-point | |---|---|---| | S-1 | **Terminate TLS** at NPM; never serve Atheos plaintext to the internet | NPM proxy host | | S-2 | **Authenticate in front of Atheos**, not just inside it — an NPM Access List / SSO so the app is not the only gate | NPM | | S-3 | **fail2ban** on the guest for failed Atheos logins and nginx 401/403s | guest + `nginx.conf` log format | | S-4 | **Rate-limit** the login endpoint in nginx (`limit_req_zone`) | `nginx.conf` | | S-5 | **Force HTTPS** redirect; set `Strict-Transport-Security` | `nginx.conf` | | S-6 | **Session hardening**: `session.cookie_secure=1`, `session.cookie_httponly=1`, `session.cookie_samesite=Lax` | `php.ini` | | S-7 | **Disable PHP dangerous functions** Atheos does not need: `exec, shell_exec, system, passthru, proc_open, popen` — *except* git needs `proc_open`/`exec`, so this needs testing before applying | `php.ini` — ⚠️ test, do not blind-apply | | S-8 | **`open_basedir`** to `/var/www/atheos` + `/tmp` to contain a file-read bug | `php.ini` | | S-9 | **nginx hardening**: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, deny dotfiles, deny `*.md`/`*.py`/`.git` serving | `nginx.conf` | | S-10 | **Protect the deploy key + config** from being served: verify `/.ssh`, `/config.php`, `/data` are not web-reachable | `nginx.conf` — **verify with curl, not by reading config** | | S-11 | **Gitea-side**: consider an IP allowlist / rate limit on the git endpoint if the box is public | Gitea/NPM | | S-12 | **Alerting** on: nginx 5xx rate, new git remote, deploy-key use, file changes under `/var/www/atheos` | not built — would need the observability stack | | S-13 | **Keep Atheos patched** — it is PHP, no releases; re-pin deliberately (see `scripts/update-atheos.sh`) | update script | | S-14 | **`allow_url_fopen=On`** is currently enabled (needed for the plugin marketplace). Turn it **off** for internet exposure unless the marketplace is genuinely required | `php.ini` | ## 3. Standing hygiene (applies now, LAN or not) - Branch protection on `armistace/blog` **master** — so a compromised editor cannot force-push master, only propose. *(Repo-side; operator-owned.)* - The deploy key is **write-enabled** by explicit choice. If push ever becomes unnecessary, flip it to read-only in the repo settings — zero code change. - No secrets in git: the private key is generated on the guest and never committed (`.gitignore`). - Agent must never create `armistace` credentials (FR-11). ## 4. Accepted risks (conscious, recorded) | Risk | Why accepted | Revisit trigger | |---|---|---| | Atheos has shell-exec capability | it is a git-integrated editor; unavoidable | if exposed publicly → apply S-2, S-7, S-8 | | Deploy key readable by the web user | required for push | if push no longer needed → set read-only | | LAN has no TLS | LAN-only scope (FR-10) | on any external exposure → S-1 | | `allow_url_fopen=On` | plugin marketplace | if the marketplace is unused → set Off | | No monitoring/alerting | out of scope; observability stack is separate | if exposed publicly → S-12 |