docs: handoff — SSH over the FIPS mesh (node-side toggle) for the node agent
Today's field test: Termux over the companion's split tunnel reaches the node's fips0 ULA and gets RST — the mesh path works end to end, port 22 is refused by the node (fips0 default-deny, no 22 in the fips.d drop-ins; sshd IPv6 listening unverified). The interim manual unblock (a source-restricted 90-ssh.nft drop-in) is documented, but the real ask is a first-class 'SSH over mesh' settings toggle in the node UI, with the drop-in lifecycle owned by the daemon, a source-scope decision (paired phones vs any mesh peer), sshd preflights, and an acceptance checklist. Companion side is done (device-wide split tunnel + the node ULA now displayed/copyable in the hub Nodes page) — the node agent is downstream.
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
# HANDOFF — SSH over the FIPS mesh (node-side toggle), 2026-08-31
|
||||||
|
|
||||||
|
**For: the node OS agent.** From the companion agent, mid-0.5.28 testing. The
|
||||||
|
user wants to SSH their node from Termux over the phone's FIPS mesh instead
|
||||||
|
of keeping Tailscale around for it — the phone side is done and verified; the
|
||||||
|
remaining work is all node-side, and it wants to be a **first-class settings
|
||||||
|
toggle**, not a hand-edited firewall rule.
|
||||||
|
|
||||||
|
## What already works (do not rebuild this)
|
||||||
|
|
||||||
|
- The companion's embedded mesh is a **device-wide split tunnel**
|
||||||
|
(`ArchyVpnService` routes `fd00::/8` for the whole phone, no per-app
|
||||||
|
filter, `allowBypass`). Termux — or any app — reaches mesh addresses with
|
||||||
|
zero setup while the tunnel is up, on-LAN and away (anchor path).
|
||||||
|
- The hub's Nodes page now **displays and copies each FIPS node's `fips0`
|
||||||
|
ULA** (committed on `companion/0.5.28`).
|
||||||
|
- Verified live today: `ssh user@<node-ULA>` from Termux answers **RST** —
|
||||||
|
the path works end-to-end; something on the node is doing the refusing.
|
||||||
|
|
||||||
|
## The diagnosis (from today's field test + code read)
|
||||||
|
|
||||||
|
1. **`fips0` is default-deny inbound.** The hardening baseline
|
||||||
|
(`/etc/fips/fips.nft`, provisioned out-of-band) rejects un-allowlisted
|
||||||
|
ports with RST — the exact symptom the web-UI drop-in's comment documents
|
||||||
|
on :80 (`core/archipelago/src/fips/config.rs` ~L237). The daemon's own
|
||||||
|
drop-ins (`/etc/fips/fips.d/80-web-ui.nft`: 80/8443/5679,
|
||||||
|
`85-app-ports.nft`: app launch ports) **do not include 22**.
|
||||||
|
2. **sshd IPv6 listening is unverified.** `fips0` is IPv6-only; a sshd pinned
|
||||||
|
to `ListenAddress 0.0.0.0` RSTs on the ULA identically. The image installs
|
||||||
|
and enables openssh-server (`image-recipe/archipelago-scripts/install-to-disk.sh`
|
||||||
|
L177/L210) with default config (binds `::`), but a preflight in the toggle
|
||||||
|
should confirm rather than assume.
|
||||||
|
|
||||||
|
**Interim manual unblock (what the user can do today, keep valid):**
|
||||||
|
`/etc/fips/fips.d/90-ssh.nft` containing `ip6 saddr <phone-ULA> tcp dport 22
|
||||||
|
accept`, then `sudo nft -f /etc/fips/fips.nft`. A daemon-owned toggle must
|
||||||
|
**own that file name/lifecycle** so a hand-added rule and the feature don't
|
||||||
|
fight over the same slot.
|
||||||
|
|
||||||
|
## The ask: a "SSH over mesh" toggle
|
||||||
|
|
||||||
|
The user's instinct (seconded here): **a setting in the FIPS/network area of
|
||||||
|
the node UI**, default **off**. Sketch:
|
||||||
|
|
||||||
|
- **UI**: a small settings card in the pattern of
|
||||||
|
`neode-ui/src/views/settings/` (see `TransportPrefsCard.vue` for a
|
||||||
|
segmented-pref card + vitest). Toggle + a source-scope selector +
|
||||||
|
preflight status rows.
|
||||||
|
- **RPC**: `fips.ssh-over-mesh.get` / `fips.ssh-over-mesh.set` (dispatch arm
|
||||||
|
in `core/archipelago/src/api/rpc/dispatcher.rs` alongside the existing
|
||||||
|
`fips.*` arms at ~L544; handler in `api/rpc/fips.rs`). Persisted with the
|
||||||
|
other fips daemon-config state.
|
||||||
|
- **Enforcement**: mirror the existing drop-in lifecycle in
|
||||||
|
`core/archipelago/src/fips/config.rs` (~L243–320): when the toggle is on,
|
||||||
|
write `/etc/fips/fips.d/90-ssh.nft` on every daemon config install and on
|
||||||
|
toggle change; when off, remove it. Reload stays
|
||||||
|
`sudo nft -f /etc/fips/fips.nft`. Never touch `80-web-ui.nft` /
|
||||||
|
`85-app-ports.nft`.
|
||||||
|
- **Source scope** (the design decision worth an issue thread):
|
||||||
|
- *Paired phones only* — restricts to the phone ULAs/npubs the node has
|
||||||
|
actually paired with. Open question: does the node durably know which
|
||||||
|
inbound peers are "its" phones? FIPS accepts inbound peers without prior
|
||||||
|
registration, so this may need a small persisted "trusted peers" list
|
||||||
|
(seeded when `fips.pair-info` is issued, or on first successful dial).
|
||||||
|
Recommended default if the data can be made reliable.
|
||||||
|
- *Custom source list* — raw ULA list, per-rule `ip6 saddr <ula> …`
|
||||||
|
entries. Escape hatch; fine to ship alongside.
|
||||||
|
- *Any mesh peer* — what the user literally asked for, but flag it
|
||||||
|
honestly in the UI: with no registration requirement, this faces port 22
|
||||||
|
at every peer that can route to the node over the mesh. If offered at
|
||||||
|
all, gate it behind the same "I understand" confirmation pattern as
|
||||||
|
other danger-zone settings.
|
||||||
|
- **Preflights, surfaced in the card**: sshd enabled + listening on IPv6
|
||||||
|
(`[::]:22` or `*:22` via `ss -tln`), and whether
|
||||||
|
`PasswordAuthentication` is on — if it is, show a keys-only recommendation
|
||||||
|
(the firewall restriction is the belt; this is the suspenders).
|
||||||
|
|
||||||
|
## Acceptance (on-device)
|
||||||
|
|
||||||
|
- [ ] Toggle on, phone on LAN: `ssh user@<node-ULA>` from Termux connects.
|
||||||
|
- [ ] Phone away from LAN (anchor path): same result.
|
||||||
|
- [ ] Toggle off: connection refused again; `90-ssh.nft` gone.
|
||||||
|
- [ ] Daemon config install (upgrade/restart) preserves the on-state and
|
||||||
|
the rule; nothing duplicated.
|
||||||
|
- [ ] Non-default source scope actually restricts (try from a second mesh
|
||||||
|
peer, or a wrong ULA).
|
||||||
|
- [ ] Settings UI survives a page reload; RPC has a vitest like
|
||||||
|
`TransportPrefsCard.test.ts`.
|
||||||
|
|
||||||
|
## Working rules
|
||||||
|
|
||||||
|
Same as the queue handoffs: small commits, tracker issue for this feature
|
||||||
|
(`ssh-over-mesh`), and the companion agent is downstream-only here — no
|
||||||
|
companion changes are required (the phone already routes and displays the
|
||||||
|
ULA). Optional nicety later, NOT part of this issue: the companion's FIPS
|
||||||
|
hub page could one day surface the toggle state — only worth it if the
|
||||||
|
`fips.ssh-over-mesh.get` RPC is trivial to add to the existing status call.
|
||||||
Reference in New Issue
Block a user