| name | cluster-ssh |
|---|---|
| description | SSH into a cardano-parts cluster's hosts over shared ControlMaster sockets to run read-only on-host diagnostics (journalctl, systemctl, zfs, cardano db tools, etc.), and move bulk state directly between hosts with wush when needed. Use when investigating node or host behavior directly on the machine — when telemetry (see the monitoring-query skill) isn't enough, or you need to inspect units, files, or local state. Requires an SRE to have opened the masters first. |
This skill ships with the cardano-parts template and is deployment-agnostic: the mechanism (the
ai-ssh recipes, ControlMaster sockets, nss_wrapper) is identical across cardano-parts clusters.
Only the host names differ — discover them per cluster (see §3). Example hostnames below are
illustrative.
Cluster hosts are reached through AWS SSM (ProxyCommand … aws ssm start-session …), so an
agent sandbox — which usually has no AWS creds and no outbound AWS network — cannot dial hosts
itself. Instead, an SRE opens persistent SSH ControlMaster sessions, and the agent rides the
shared sockets. The agent never needs AWS creds: the master process holds the SSM tunnel.
This is the escalation path from the monitoring-query skill: use telemetry first; come here when
you need to read units/files/DB state on the box. Default to read-only commands (journalctl,
systemctl status, zfs list, jq, cat, ls, db-analyser --help). Anything that mutates
state (restart, edit, destroy, write) must be confirmed with the SRE first.
From the repo root the SRE runs the ai-ssh recipe with the host(s) to expose:
just ai-ssh <host> [<host> ...] # e.g. just ai-ssh group1-role-a-1
just ai-ssh-list # show which masters are live
just ai-ssh-close [<host> ...] # close all, or the named hostsai-ssh (re)generates an agent ssh config and opens a backgrounded master per host. It prints the
two paths the agent needs:
- Agent config:
$AI_SSH_DIR/ssh_config_ai - Socket dir:
$AI_SSH_DIR(sockets namedcm-<hostname>)
AI_SSH_DIR defaults to a repo-local .ssh-ai/ but the SRE may set it to a path your sandbox can
read (e.g. a mounted share). Don't assume a path — use the one the recipe printed, or ask the
SRE for AI_SSH_DIR. The config embeds an absolute ControlPath, so it resolves to the same socket
from your sandbox despite a different $HOME/cwd.
Connect with the generated config; the master does the I/O, so no creds/tunnel are needed:
CFG="$AI_SSH_DIR/ssh_config_ai" # the path the recipe printed
ssh -F "$CFG" <host> '<read-only command>'Always verify the master is up first (non-dialing — fails fast if the SRE hasn't opened it):
ssh -F "$CFG" -O check <host> # "Master running (pid=…)" = goodOpenSSH aborts with No user exists for uid … when the sandbox uid isn't in /etc/passwd. Fix with
nss_wrapper (a fake passwd/group + LD_PRELOAD). One-time bootstrap, then wrap every ssh call:
NW=$(nix build --no-link --print-out-paths nixpkgs#nss_wrapper)
printf 'agent:x:%s:%s::%s:/bin/sh\n' "$(id -u)" "$(id -g)" "$HOME" > /tmp/nwpasswd
printf 'agent:x:%s:\n' "$(id -g)" > /tmp/nwgroup
# /tmp/sshw — invoke as: bash /tmp/sshw <host> '<cmd>'
cat > /tmp/sshw <<EOF
#!/usr/bin/env bash
exec env LD_PRELOAD="$NW/lib/libnss_wrapper.so" \\
NSS_WRAPPER_PASSWD=/tmp/nwpasswd NSS_WRAPPER_GROUP=/tmp/nwgroup \\
USER=agent LOGNAME=agent ssh -F "$CFG" "\$@"
EOF
bash /tmp/sshw <host> -O check # verify, then run real commandsIf your sandbox already has a passwd entry, skip the wrapper and use ssh -F "$CFG" directly.
just ssh-list name '.*' # all node names
just ssh-list name 'preprod.*' # filtered
nix eval --json '.#nixosConfigurations' --apply builtins.attrNames | jqNaming: <env><n>-<role>-<az>-1, e.g. leios1-rel-a-1, preprod2-bp-b-1. Block producers
(-bp-) sit behind their relays (-rel-); an SRE may only have opened a subset — check
just ai-ssh-list.
The agent reaches hosts only through the SRE's SSM/ControlMaster path, and cluster hosts generally
cannot SSH each other directly (SSM-only; inter-host port 22 is typically closed). So relaying a
large file host→agent→host is slow and fragile — it crosses the SSM tunnel twice and dies if a master
flaps. When you need to move bulk state between two machines — e.g. transplanting a node's database /
state directory from a healthy donor onto a broken one — use wush if it's installed
(command -v wush; on NixOS it can be added to system packages or fetched via nix).
wush is a WireGuard-powered peer-to-peer transfer + shell tool. It stands up its own encrypted
overlay between the two hosts (direct UDP, or DERP-relayed when there's no direct path), so bytes flow
host-to-host, bypassing both the SSM bottleneck and the missing inter-host SSH — typically orders
of magnitude faster than relaying through the agent.
Model: one side runs wush serve (it prints an --auth-key); the other runs a client
(wush rsync, wush cp, wush ssh, wush port-forward) with that key. A leading : denotes a path
on the server:
# On the SERVER host (the one you transfer to/from). Run it backgrounded; it prints the auth key
# on the line just after "Picked DERP region …".
wush serve # → auth key: <base58 string>
# On the CLIENT host, with that key (extra rsync flags go after `--`):
wush rsync --auth-key <key> :/srv/path /local/path -- -a # pull (server → client)
wush rsync --auth-key <key> /local/path :/srv/path -- -a # push (client → server)Driving it from the agent: start wush serve on one host as a backgrounded SSM session, read its auth
key from the output, then run the wush rsync client on the other host. One running serve + key
handles multiple parallel clients, so a single donor can fan its data out to several recipients at once.
Transplanting live state safely:
- Briefly stop the service on the source for an on-disk-consistent copy, then restart — never copy
a database that's being written. To shorten the source's downtime, stage a scratch copy while it's
stopped and serve the transfer from that. A plain
cpof the state dir may be near-instant on ZFS (acopy_file_rangeblock-clone) — but only with OpenZFS ≥ 2.2, theblock_cloningpool feature,zfs_bclone_enabled=1, and acpthat does reflinks (GNU coreutils ≥ 9); otherwise it's a full copy. Don't assume it — check (zpool get feature@block_cloning,cat /sys/module/zfs/parameters/zfs_bclone_enabled), or just keep the source stopped for the whole transfer. For a guaranteed-instant and consistent point-in-time source,zfs snapshotworks regardless ofcpbehavior — but remember tozfs destroyit afterward. rsync -apreserves owner/perms; cluster hosts share the same service uid/gid, so ownership comes across correctly. State dirs carry no per-host identity (keys live elsewhere), so they're safe to clone between same-network nodes.- This mutates the target — confirm with the SRE first; read-only-by-default still applies.
- Cleanup: stop the server with
pkill -x wush— notpkill -f "wush serve", which also matches your own command line and kills your shell. Remove any scratch copies you staged.
- Read-only by default. Surface anything mutating to the SRE before running it.
- Hosts are UTC — no
--utcneeded forjournalctl -S/-U. - journald JSON lines are duplicated (×2) in some setups — sanity-check counts.
- Capture long output to a file, then grep/parse it — don't re-run expensive
journalctl/curl. - Node log format (JSON vs human) is a per-host tracer choice and can change on redeploy — sample a
line before parsing (same caveat as the
monitoring-queryskill). - Masters expire after
AI_SSH_PERSIST(default 2h); if-O checkfails, ask the SRE to re-runjust ai-ssh <host>. - Don't relay bulk data through your sandbox — to move large state between hosts use
wush(see Moving bulk data between hosts), which goes host-to-host and survives master flaps.