# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this repository is This is **not an application codebase** — there's nothing to build, lint, or test. It's the operations/documentation repo for administering two real, live hosts remotely over SSH: - **`alpha.jayfield.org`** (`89.58.8.149`) — public VPS, Ubuntu 24.04 LTS, reached via `ssh -p 10022 jens@alpha.jayfield.org`. Runs authoritative DNS (BIND9), a full mail stack (Postfix/Dovecot/rspamd/MariaDB), Apache reverse-proxying into a Docker layer (Portainer, Nextcloud, Gitea, a plain nginx container), and fail2ban. - **`vlda-01`** — home intranet server (Unraid 7.3.2), reached via `ssh root@vlda-01` from the same LAN. Not part of `alpha`'s public `jayfield.org` zone — lives under the private, unresolvable `home.jayfield.org` namespace. Currently the pull side of the Gitea backup from `alpha`. **All actual changes happen live on those hosts over SSH, not in this repo.** This repo records what's true afterward and keeps reference copies of deployed scripts/configs. There is no local checkout of any live service config to edit — the working pattern for every task is: SSH in → make the change on the real host → verify it live (curl a vhost, run `sanity-check.sh`, check a real log line) → record it here. ## Document map (read the right one, not all of them) | File | Purpose | Rule | |---|---|---| | `README.md` | **Live state** — what's actually running on `alpha` right now | Source of truth when it disagrees with `SETUP.md` | | `SETUP.md` | Fresh-install build runbook (currently: from-scratch on 24.04) | How to reproduce the host from nothing, not what's live | | `TODO.md` | Full chronological audit log of every bug/fix/hardening pass | **Append-only. Never consolidate or summarize existing entries unprompted** — this has been corrected before; keep the full narrative detail even when it's long | | `SYNC-PLAN.md` | Point-in-time plan for the `vlda-01` → `alpha` intranet migration | Move a service's write-up into `README.md` once it's actually built and live; only update the plan for what's still pending | | `MAIL-ACCOUNTS.md` | Day-to-day recipe for adding/disabling mail accounts | References `SETUP.md` §3 for the underlying schema | | `DIFF.md` | One-time (2026-07-19) diff-vs-clean-install investigation | Explicitly a snapshot, not a live inventory — don't treat its findings as current without re-checking | | `scripts/` | Reference copies of everything deployed on `alpha` and its clients | **Not auto-deployed** — see below | ## The `scripts/` deployment workflow `scripts/` is split by host (`scripts/alpha/`, `scripts/clients//`) because each host has a genuinely different OS with different persistence/deployment conventions — each subdirectory's own `README.md` states the exact OS/version and the live deployment path for every file. **Editing a file under `scripts/` changes nothing on the real host.** After editing, the actual workflow is: 1. Deploy the change to the live host (`scp`/`cat > file` over SSH to the path listed in that host's `README.md`). 2. `diff` the deployed copy against the repo copy — they must match byte-for-byte before moving on. 3. Test the actual behavior live (run the script, hit the endpoint, reboot if it's boot-triggered) — not just "it deployed without error." 4. Only then update `README.md`/`TODO.md` to describe the change. `vlda-01` (Unraid) needs particular care here: `/` and `/usr` are RAM-backed and don't survive a reboot, and `/boot` (the persistent USB stick) is VFAT with no Unix execute bit — scripts there are deployed via the **User Scripts** plugin and invoked with `bash `, never `exec`. See `scripts/clients/vlda-01/README.md` before touching anything there. ## Verifying a change is the closest thing to a test suite There's no CI here — "does it work" is checked directly against the live host: - `sudo /usr/local/sbin/sanity-check.sh` on `alpha` — checks the whole stack (packages, DNS/DNSSEC, mail, MariaDB, fail2ban, every public vhost by expected HTTP status, all Docker containers, plus a version dump of every involved service) and prints PASS/WARN/FAIL. Runs automatically once per boot via `sanity-check.service`; run it manually after any config change that doesn't itself trigger a reboot. - `sudo /usr/local/sbin/attacker-check.py` — weekly fail2ban log analysis (also runs on its own via cron); safe to run manually any time. - For anything touching the mail stack, DNS, or a vhost: a real end-to-end check beats trusting the config on paper (e.g. an actual `dig` against the authoritative server, a real test email round-trip via `/var/log/mail.log`, a real `curl` against the public hostname) — this repo's history (`TODO.md`) has repeated cases of a config that "looked" correct but silently didn't work. ## A few standing facts worth knowing before editing docs - `jayfield.org` (public, this host's zone) and `home.jayfield.org` (private home-LAN namespace, `vlda-01` and friends) are deliberately separate — see `README.md`'s "Domain namespace" section. - Gitea (`git.jayfield.org`) is the canonical git host for this repo itself (migrated from `vlda-01`, which keeps a cold, stopped backup copy). There is no GitHub remote. - SSH access details (ports, users) belong in `README.md`/host `README.md`s, not duplicated here — check there rather than assuming.