nixos/hosts: add README documenting host roles + deploy flow
Deferred Phase-1-completion task from the de-platform-from-GitHub roadmap (vimwiki/diary/2026-05-03.md). Documents: - per-host role + current services (sunken-ship, phantom-ship, vps-relay, distant-shore, foreign-port, daniel-macbook-air, wsl); - ZT mesh topology + ASCII overview; - the auto-rebuild path (dm-pull-deploy push from sunken-ship → pull/ rebuild on roles.default hosts within ~15 m); - the manual clan-cli flow, including the env -u SSH_AUTH_SOCK and --no-check gotchas we hit in practice; - the vps-relay reverse-proxy pattern for new public apps; - SSH key quick-reference.
This commit is contained in:
parent
05896f6d3b
commit
a903d76f65
1 changed files with 206 additions and 0 deletions
206
nixos/hosts/README.md
Normal file
206
nixos/hosts/README.md
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
# Hosts
|
||||||
|
|
||||||
|
Per-host NixOS configs for the homelab and admin Mac. Each `<host>.nix`
|
||||||
|
declares the host's role and services; the `<host>-hardware.nix` siblings
|
||||||
|
(where present) describe disks, kernel modules, firmware. Bootstrap +
|
||||||
|
disko configs live one level up in `../`.
|
||||||
|
|
||||||
|
## Topology
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────┐
|
||||||
|
│ vps-relay (Hetzner, public IP, ZT peer) │
|
||||||
|
public traffic ──TLS:443──────│ Caddy + Let's Encrypt → reverse_proxy │
|
||||||
|
│ over ZeroTier to a clan backend │
|
||||||
|
└──────────────────┬─────────────────────────┘
|
||||||
|
│ (ZeroTier mesh)
|
||||||
|
│
|
||||||
|
┌──────────────────────┬───────────┴───────────┬──────────────────────┐
|
||||||
|
│ │ │ │
|
||||||
|
sunken-ship phantom-ship distant-shore foreign-port
|
||||||
|
(LAN, wifi) (LAN, wired) (LAN, wifi) (LAN, wifi)
|
||||||
|
ZT controller NAT for blank slate blank slate
|
||||||
|
media + mulbo rusty-anchor (room to grow) (room to grow)
|
||||||
|
services hub
|
||||||
|
|
||||||
|
── outside the clan ──────────────────────────────────────────────────────────────────────
|
||||||
|
Daniel-Macbook-Air rusty-anchor
|
||||||
|
(admin, runs clan-cli) (downstream of phantom-ship's NAT)
|
||||||
|
```
|
||||||
|
|
||||||
|
ZeroTier IPv6 addresses for the four clan machines are declared in
|
||||||
|
`../../flake-modules/clan.nix` (`sunkenShipZTv6` / `phantomShipZTv6` /
|
||||||
|
`vpsRelayZTv6` / etc.). They land in every host's `/etc/hosts` as
|
||||||
|
`<machine>.clan` so data-mesher and ad-hoc SSH can resolve over the mesh.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hosts
|
||||||
|
|
||||||
|
### sunken-ship · media + ZT controller
|
||||||
|
|
||||||
|
- **Hardware:** see `sunken-ship-hardware.nix`. WiFi-only, no LUKS (boots
|
||||||
|
unattended).
|
||||||
|
- **Network:** LAN over WiFi, on the ZT mesh as the **controller** (manages
|
||||||
|
ZT membership for the whole fleet). ZT IPv6 is the clan's "internet"
|
||||||
|
target for `clan machines update`.
|
||||||
|
- **Role:** media + the long-running personal services that don't fit on
|
||||||
|
phantom-ship.
|
||||||
|
- **Current services:** `navidrome` (subsonic API, `/srv/music`), `uxplay`
|
||||||
|
(AirPlay receiver), `mulbo-server` (+ `-pull` / `-backfill` / `-enrich`
|
||||||
|
timers), `fitness-bot` (+ `-pull` / `-shipyard` variants),
|
||||||
|
`dm-pull-deploy-push` (announces origin/main rev to the mesh every 15 m).
|
||||||
|
|
||||||
|
### phantom-ship · services hub + LAN NAT
|
||||||
|
|
||||||
|
- **Hardware:** see `phantom-ship-hardware.nix`. WiFi for WAN, wired
|
||||||
|
ethernet (`enp0s31f6`) serves the lab subnet (NAT + dnsmasq for
|
||||||
|
`rusty-anchor`).
|
||||||
|
- **Network:** LAN over WiFi, on the ZT mesh. Backends are exposed only on
|
||||||
|
the ZT interface (`firewall.interfaces."zt+".allowedTCPPorts = [ … ]`)
|
||||||
|
so vps-relay's Caddy can reach them. WAN side stays closed.
|
||||||
|
- **Role:** where new self-hosted apps default to going. Hosts a growing
|
||||||
|
list of mini-app backends + a couple of long-running daemons.
|
||||||
|
- **Current services:** `forgejo` (`git.dannydannydanny.me`),
|
||||||
|
`claude-channels` (Telegram bridge for `@HarakatBot`),
|
||||||
|
`hara-gmail-mcp` + `hara-heartbeat` (timer),
|
||||||
|
Mini-App backends (`shelfish`, `scuttle`, `bananasimulator`,
|
||||||
|
`komtolk`, `escape-hormuz`, `bon`), `ollama` (local LLM), `shipyard`,
|
||||||
|
`dnsmasq` (lab subnet DHCP/DNS). `openclaw-gateway` is disabled —
|
||||||
|
superseded by `claude-channels` but kept for easy rollback.
|
||||||
|
|
||||||
|
### vps-relay · public reverse proxy
|
||||||
|
|
||||||
|
- **Hardware:** Hetzner Cloud vServer (BIOS-boot, virtio). Disk via
|
||||||
|
`../disko-cloud.nix`.
|
||||||
|
- **Network:** public IP `89.167.39.251`. Inbound: SSH/22, HTTP/80,
|
||||||
|
HTTPS/443 only. fail2ban guards SSH. Outbound to clan backends over ZT.
|
||||||
|
- **Role:** terminates public TLS, reverse-proxies subdomains over ZT to
|
||||||
|
whichever clan host runs the backend. **No application data ever lands
|
||||||
|
here** — this box is a relay. New public app = add a `virtualHosts`
|
||||||
|
entry + a GoDaddy A record pointing at `89.167.39.251`.
|
||||||
|
- **Current vhosts:** `navidrome.`, `bbbot.`, `shelfish.`, `scuttle.`,
|
||||||
|
`bananasimulator.`, `komtolk.`, `git.`, `escapehormuz.`, etc.
|
||||||
|
|
||||||
|
### distant-shore · ThinkPad X13 Gen 2, blank slate
|
||||||
|
|
||||||
|
- **Hardware:** see `distant-shore-hardware.nix`. Intel i5-1145G7, 16 GB.
|
||||||
|
WiFi-only, headless, no LUKS. Secure-Boot-chained boot (shim + MOK,
|
||||||
|
see comments in `distant-shore.nix`).
|
||||||
|
- **Network:** LAN over WiFi, on the ZT mesh.
|
||||||
|
- **Role:** _to be assigned_. In the clan inventory; auto-rebuilds via
|
||||||
|
dm-pull-deploy. Drop a service in to give it a purpose.
|
||||||
|
|
||||||
|
### foreign-port · laptop, blank slate (WIP)
|
||||||
|
|
||||||
|
- **Hardware:** see `foreign-port-hardware.nix`. WiFi-only, headless,
|
||||||
|
no LUKS. Vendor-signed-shim boot chain.
|
||||||
|
- **Status:** still being wired up — not in the clan inventory yet.
|
||||||
|
- **Role:** _to be assigned_, same flow as `distant-shore`.
|
||||||
|
|
||||||
|
### daniel-macbook-air · admin
|
||||||
|
|
||||||
|
- **Hardware:** MacBook Air (the daily driver).
|
||||||
|
- **Role:** outside the clan. Runs `clan machines update` to push to
|
||||||
|
the servers + holds the SSH keys that authorize root@ on each clan
|
||||||
|
host. Also a ZT peer.
|
||||||
|
|
||||||
|
### wsl
|
||||||
|
|
||||||
|
- **Role:** WSL development environment (legacy / occasional).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Automatic (the default)
|
||||||
|
|
||||||
|
`dm-pull-deploy` (clan-community module wired in `clan.nix`):
|
||||||
|
|
||||||
|
1. **Push announcement:** sunken-ship's `dm-pull-deploy-push` timer runs
|
||||||
|
`dm-send-deploy` every 15 m. It signs and broadcasts the current
|
||||||
|
`origin/main` rev over the data-mesher gossip protocol.
|
||||||
|
2. **Pull + rebuild:** each `roles.default` machine (currently
|
||||||
|
`sunken-ship`, `phantom-ship`) runs a `.path` watcher that fires when
|
||||||
|
the gossiped rev changes; it `git fetch`es and `nixos-rebuild switch`es.
|
||||||
|
|
||||||
|
So **a push to `origin/main` rolls out within ~15 m** on the two
|
||||||
|
production hosts. No SSH-from-Mac required.
|
||||||
|
|
||||||
|
`vps-relay` and `distant-shore` are **not** in `roles.default` — they
|
||||||
|
need a manual deploy (see below) until/unless their role changes.
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
From `~/dotfiles` on the Mac:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix run 'git+https://git.clan.lol/clan/clan-core#clan-cli' -- \
|
||||||
|
machines update <host>
|
||||||
|
```
|
||||||
|
|
||||||
|
Caveats encountered in practice:
|
||||||
|
|
||||||
|
- The Mac's `ssh-agent` often has the wrong key loaded for clan deploys.
|
||||||
|
Prefix with `env -u SSH_AUTH_SOCK` to force `~/.ssh/config` identity
|
||||||
|
selection.
|
||||||
|
- A nixpkgs bump may register a new generation but refuse to live-switch
|
||||||
|
due to "switch inhibitors". Add `--no-check` to force.
|
||||||
|
- `vps-relay` only accepts `~/.ssh/id_ed25519_sunken_ship` (the Mac's
|
||||||
|
copy of sunken-ship's authorized key). The agent's other keys won't
|
||||||
|
open it.
|
||||||
|
|
||||||
|
Putting both together:
|
||||||
|
|
||||||
|
```
|
||||||
|
env -u SSH_AUTH_SOCK nix run 'git+https://git.clan.lol/clan/clan-core#clan-cli' \
|
||||||
|
-- machines update phantom-ship --no-check
|
||||||
|
```
|
||||||
|
|
||||||
|
### From sunken-ship
|
||||||
|
|
||||||
|
`vps-relay` was originally only reachable from sunken-ship's SSH key.
|
||||||
|
That still works as a fallback — SSH to sunken-ship and run the same
|
||||||
|
`nix run … -- machines update vps-relay` command from `/etc/dotfiles`
|
||||||
|
there. The dotfiles checkout at `/etc/dotfiles` is maintained by
|
||||||
|
dm-pull-deploy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Public traffic pattern
|
||||||
|
|
||||||
|
```
|
||||||
|
user → DNS *.dannydannydanny.me → 89.167.39.251 (vps-relay)
|
||||||
|
→ Caddy (Let's Encrypt, ports 80/443)
|
||||||
|
→ reverse_proxy http://[<backend ZT IPv6>]:<port>
|
||||||
|
→ service on sunken-ship or phantom-ship
|
||||||
|
```
|
||||||
|
|
||||||
|
To add a new public app:
|
||||||
|
|
||||||
|
1. Add a `virtualHosts` entry to `vps-relay.nix` pointing at the
|
||||||
|
backend's ZT IPv6 and port.
|
||||||
|
2. Add the GoDaddy A record `<sub>.dannydannydanny.me → 89.167.39.251`.
|
||||||
|
3. Run the backend on the chosen host. Either:
|
||||||
|
- bind to `127.0.0.1:<port>` (if backend + Caddy are co-resident — not
|
||||||
|
the case here), **or**
|
||||||
|
- bind to `0.0.0.0` (or `::`) and add the port to
|
||||||
|
`networking.firewall.interfaces."zt+".allowedTCPPorts` on the
|
||||||
|
backend host so only the ZT interface accepts inbound.
|
||||||
|
4. Push dotfiles. Production hosts auto-rebuild via dm-pull-deploy.
|
||||||
|
vps-relay needs a manual `clan machines update vps-relay`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SSH keys (quick reference)
|
||||||
|
|
||||||
|
- **`~/.ssh/id_ed25519_phantom_ship`** (Mac) — authorized as `danny@` and
|
||||||
|
`root@` on phantom-ship.
|
||||||
|
- **`~/.ssh/id_ed25519_sunken_ship`** (Mac) — authorized as `danny@` (and
|
||||||
|
via root mirror) on sunken-ship; also the authorized key on `vps-relay`.
|
||||||
|
- **sunken-ship `~/.ssh/id_ed25519`** — sunken-ship's own key; used by
|
||||||
|
cluster-internal ops (mulbo-pull, dm-send-deploy, fallback path for
|
||||||
|
vps-relay deploys).
|
||||||
|
- **`~/.ssh/id_ed25519_github`** (Mac) — GitHub auth, not clan.
|
||||||
|
|
||||||
|
Authorized-keys lists live in each host's `users.users.{danny,root}.openssh.authorizedKeys.keys`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue