feat(ssh): full homelab alias set on the Mac + separate interactive key

config/ssh_homelab now carries the curated alias set from ~/bin/CLAUDE.md
(pve nodes, machines, key CTs, switch, tailscale hosts). Interactive auth
uses a NEW key, id_ed25519_homelab, because the existing id_ed25519 is
rrsync-jailed on pve173 and sshd honours the first matching authorized_keys
line per key — one key can't be both a jail and a shell there.

- setup.sh generates id_ed25519_homelab (idempotent)
- backup.sh pins the jailed key: -o IdentitiesOnly=yes -i $HOME/.ssh/id_ed25519
- scripts/authorize-mac-key.sh (run on steel141) appends the pubkey on every
  LAN host via claude's aliases; wired into run.sh
- bedroom alias: seth -> root (matches steel141; was undocumented)

Verified from the Mac: 12 aliases land as the right user@host; the backup key
is refused a shell by rrsync (checked with id — rrsync whitelists true);
backup.sh still runs. Offline at push time: bedroom thinkcentre seth-pi
pinail openclaw2 — re-run authorize-mac-key.sh when they're up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mortdecai
2026-09-15 20:31:25 -04:00
parent 43877eb78f
commit 2028508cb9
7 changed files with 82 additions and 4 deletions
+1
View File
@@ -33,6 +33,7 @@ laptop" shape).
Pre-change inventory: `docs/reference/inventory-2026-09-15.md`. Pre-change inventory: `docs/reference/inventory-2026-09-15.md`.
- Backup: `~/Music/Ableton` + `~/Documents` -> `pve173:/tank/backups/mac` nightly - Backup: `~/Music/Ableton` + `~/Documents` -> `pve173:/tank/backups/mac` nightly
03:30 via launchd; key is rrsync-jailed; sanoid keeps history (tank_media template). 03:30 via launchd; key is rrsync-jailed; sanoid keeps history (tank_media template).
- ssh aliases (`config/ssh_homelab`, included from `~/.ssh/config`) mirror the `~/bin/CLAUDE.md` set. Two keys: `id_ed25519` = backup (rrsync-jailed on pve173), `id_ed25519_homelab` = interactive. `scripts/authorize-mac-key.sh` (from steel141) pushes the interactive key; re-run it when an offline host comes back.
- SparkFun PPP service left in place (macOS won't remove the sole service on a port; harmless). - SparkFun PPP service left in place (macOS won't remove the sole service on a port; harmless).
## Conventions ## Conventions
+2
View File
@@ -19,6 +19,8 @@ Format: `YYYY-MM-DD: <decision> — <why>`
- 2026-09-15: **Unattended sudo = temporary `/etc/sudoers.d/mac-setup` (NOPASSWD), not pty password injection.** Piping the password into `ssh -tt` raced and hung 14 min on the real run (passed a fast test). The drop-in is validated before activation, removed on every run.sh exit path, and self-destructs on the Mac after 40 min. See run.sh / _install_sudoers.sh. - 2026-09-15: **Unattended sudo = temporary `/etc/sudoers.d/mac-setup` (NOPASSWD), not pty password injection.** Piping the password into `ssh -tt` raced and hung 14 min on the real run (passed a fast test). The drop-in is validated before activation, removed on every run.sh exit path, and self-destructs on the Mac after 40 min. See run.sh / _install_sudoers.sh.
- 2026-09-15: Mac's backup key on pve173 is **rrsync-restricted** (`command="/usr/bin/rrsync /tank/backups/mac"`) — a travelling laptop's key must never be unrestricted root on the tank host. Proven: an interactive command over that key is refused. - 2026-09-15: Mac's backup key on pve173 is **rrsync-restricted** (`command="/usr/bin/rrsync /tank/backups/mac"`) — a travelling laptop's key must never be unrestricted root on the tank host. Proven: an interactive command over that key is refused.
- 2026-09-15: **Optional/cosmetic steps must be non-fatal under `set -e`.** The SparkFun removal (macOS refuses) and the GUI-domain `launchctl bootstrap` (flaky over SSH) both aborted the run until guarded with `|| warn`. Rule for this repo: anything that can legitimately fail without breaking the goal gets a best-effort guard. - 2026-09-15: **Optional/cosmetic steps must be non-fatal under `set -e`.** The SparkFun removal (macOS refuses) and the GUI-domain `launchctl bootstrap` (flaky over SSH) both aborted the run until guarded with `|| warn`. Rule for this repo: anything that can legitimately fail without breaking the goal gets a best-effort guard.
- 2026-09-15: **Two ssh keys on the Mac: `id_ed25519` (backup, rrsync-jailed) + `id_ed25519_homelab` (interactive, `Host *` default).** sshd honours the first matching authorized_keys line per key, so one key can't be both jailed and a shell on pve173. `backup.sh` pins the jailed key with `-i` + `IdentitiesOnly`; verify with `id`, not `true` (rrsync whitelists `true` as a ping). Key push = `scripts/authorize-mac-key.sh` from steel141 (uses claude's aliases; pve nodes share `/etc/pve/priv/authorized_keys`, so one write covers all four).
- 2026-09-15: Mac ssh aliases mirror the curated set in `~/bin/CLAUDE.md` (not claude's full `~/.ssh/config`) — stale/undocumented entries (nixos, thinkpad-nano, mortdec-auth, vast) left out. `bedroom` = root, matching steel141's alias (was `seth`, undocumented).
- 2026-09-15: `defaults displaysleep` on AC = 30 min, not 0 — display sleep doesn't stop Live's audio; only system `sleep` does. - 2026-09-15: `defaults displaysleep` on AC = 30 min, not 0 — display sleep doesn't stop Live's audio; only system `sleep` does.
## Deferred / Rejected ## Deferred / Rejected
+60 -3
View File
@@ -1,5 +1,10 @@
# Included from ~/.ssh/config. Key ~/.ssh/id_ed25519 is generated by setup.sh; # Included from ~/.ssh/config (setup.sh adds the Include line). Two keys, both made by setup.sh:
# only pve173 is authorized so far (rrsync-restricted, for backup.sh). # ~/.ssh/id_ed25519_homelab interactive — authorized on the hosts below by scripts/authorize-mac-key.sh
# ~/.ssh/id_ed25519 backup only — rrsync-jailed on pve173; backup.sh pins it with -i
# They must stay separate: sshd honours the FIRST matching authorized_keys line per key, so one key
# can't be both jailed (nightly rsync) and unrestricted (a shell) on pve173.
# --- Proxmox nodes ---
Host pve173 Host pve173
HostName 192.168.0.173 HostName 192.168.0.173
User root User root
@@ -12,12 +17,64 @@ Host pve197
Host pve241 Host pve241
HostName 192.168.0.241 HostName 192.168.0.241
User root User root
# --- Machines ---
Host steel141 Host steel141
HostName 192.168.0.141 HostName 192.168.0.141
User seth User seth
Host bedroom Host bedroom
HostName 192.168.0.235 HostName 192.168.0.235
User root
Host vdj-rig
HostName 192.168.0.143
User seth User seth
Host thinkcentre
HostName 192.168.0.211
User seth
Host seth-pi
HostName 192.168.0.102
User seth
Host pinail
HostName 192.168.0.203
User pi
# TP-Link switch: password only (no pubkey support) — $HOMELAB_PASSWORD
Host switch
HostName 192.168.0.250
User admin
PubkeyAuthentication no
# --- Key CTs ---
Host caddy
HostName 192.168.0.185
User root
Host openclaw2
HostName 192.168.0.175
User root
Host rtorrent
HostName 192.168.0.213
User root
Host emby
HostName 192.168.0.215
User root
Host mcsmanager
HostName 192.168.0.244
User root
Host pihole
HostName 192.168.0.153
User root
Host arr
HostName 192.168.0.158
User root
# --- Tailscale-only (dead until Tailscale is logged in on the Mac — docs/manual-checklist.md) ---
Host yoga
HostName 100.113.203.99
User seth
Host bebop
HostName 100.78.2.106
User seth
Host * Host *
IdentityFile ~/.ssh/id_ed25519 IdentityFile ~/.ssh/id_ed25519_homelab
StrictHostKeyChecking accept-new
ServerAliveInterval 30 ServerAliveInterval 30
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# From steel141: put the Mac's interactive pubkey on every LAN host in config/ssh_homelab, via
# claude's own aliases (same names, same target users). Idempotent; called by run.sh, safe alone.
# steel141 itself is the one exception: seth's authorized_keys needs sudo from the claude account.
set -euo pipefail
PUB=$(ssh -o BatchMode=yes mac cat .ssh/id_ed25519_homelab.pub)
[[ $PUB == ssh-ed25519\ AAAA* ]] || { echo "no ~/.ssh/id_ed25519_homelab on the Mac — run setup.sh first"; exit 1; }
ADD='umask 077; mkdir -p ~/.ssh; if grep -qF "$PUB" ~/.ssh/authorized_keys 2>/dev/null; then echo "[skip] $H"; else echo "$PUB" >> ~/.ssh/authorized_keys; echo "[set] $H"; fi'
for H in pve112 pve173 pve197 pve241 bedroom vdj-rig thinkcentre seth-pi pinail caddy openclaw2 rtorrent emby mcsmanager pihole arr; do
ssh -o BatchMode=yes -o ConnectTimeout=5 "$H" "PUB='$PUB' H=$H bash -s" <<<"$ADD" || echo "[FAIL] $H"
done
sudo -n -u seth env PUB="$PUB" H=steel141 bash -c "$ADD"
+3 -1
View File
@@ -7,4 +7,6 @@ src=()
for d in "$HOME/Music/Ableton" "$HOME/Documents"; do [[ -d $d ]] && src+=("$d"); done for d in "$HOME/Music/Ableton" "$HOME/Documents"; do [[ -d $d ]] && src+=("$d"); done
[[ ${#src[@]} -gt 0 ]] || { echo "nothing to back up yet"; exit 0; } [[ ${#src[@]} -gt 0 ]] || { echo "nothing to back up yet"; exit 0; }
# Remote path is relative to the rrsync root (/tank/backups/mac) set in root's authorized_keys on pve173. # Remote path is relative to the rrsync root (/tank/backups/mac) set in root's authorized_keys on pve173.
exec /opt/homebrew/bin/rsync -a --delete -e 'ssh -o BatchMode=yes' "${src[@]}" root@192.168.0.173:/ # -i pins the jailed key; the Host * default (id_ed25519_homelab) has a root shell on pve173 and must NOT be offered first.
# Check the jail holds: ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 root@192.168.0.173 id -> "rrsync error: ... does not run rsync" (NOT `true`: rrsync whitelists it as a ping).
exec /opt/homebrew/bin/rsync -a --delete -e "ssh -o BatchMode=yes -o IdentitiesOnly=yes -i $HOME/.ssh/id_ed25519" "${src[@]}" root@192.168.0.173:/
+1
View File
@@ -27,6 +27,7 @@ cleanup; trap - EXIT
ssh -o BatchMode=yes mac 'test ! -e /etc/sudoers.d/mac-setup && echo "[ok] temp sudoers removed" || echo "[WARN] temp sudoers STILL PRESENT"' ssh -o BatchMode=yes mac 'test ! -e /etc/sudoers.d/mac-setup && echo "[ok] temp sudoers removed" || echo "[WARN] temp sudoers STILL PRESENT"'
mkdir -p .backup/mac && rsync -a mac:.mac-setup-backup/ .backup/mac/ mkdir -p .backup/mac && rsync -a mac:.mac-setup-backup/ .backup/mac/
scripts/authorize-mac-key.sh
[[ ${1:-} == --no-tank ]] && exit 0 [[ ${1:-} == --no-tank ]] && exit 0
PUB=$(ssh mac cat .ssh/id_ed25519.pub) PUB=$(ssh mac cat .ssh/id_ed25519.pub)
ssh pve173 "PUB='$PUB' bash -s" < scripts/tank-side.sh ssh pve173 "PUB='$PUB' bash -s" < scripts/tank-side.sh
+3
View File
@@ -65,6 +65,9 @@ mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
if [[ -f $HOME/.ssh/id_ed25519 ]]; then log skip "ssh key exists"; else if [[ -f $HOME/.ssh/id_ed25519 ]]; then log skip "ssh key exists"; else
ssh-keygen -t ed25519 -N '' -C "seth@$HOSTNAME_WANT" -f "$HOME/.ssh/id_ed25519" >/dev/null; log set "generated ~/.ssh/id_ed25519"; CHANGED=1 ssh-keygen -t ed25519 -N '' -C "seth@$HOSTNAME_WANT" -f "$HOME/.ssh/id_ed25519" >/dev/null; log set "generated ~/.ssh/id_ed25519"; CHANGED=1
fi fi
if [[ -f $HOME/.ssh/id_ed25519_homelab ]]; then log skip "ssh homelab key exists"; else # interactive key; see config/ssh_homelab header
ssh-keygen -t ed25519 -N '' -C "seth@$HOSTNAME_WANT-homelab" -f "$HOME/.ssh/id_ed25519_homelab" >/dev/null; log set "generated ~/.ssh/id_ed25519_homelab"; CHANGED=1
fi
if [[ -f $HOME/.ssh/config ]] && grep -q '^Include ~/mac/config/ssh_homelab' "$HOME/.ssh/config"; then log skip "ssh config include"; else if [[ -f $HOME/.ssh/config ]] && grep -q '^Include ~/mac/config/ssh_homelab' "$HOME/.ssh/config"; then log skip "ssh config include"; else
[[ -f $HOME/.ssh/config ]] && cp "$HOME/.ssh/config" "$BK/ssh_config-$TS" [[ -f $HOME/.ssh/config ]] && cp "$HOME/.ssh/config" "$BK/ssh_config-$TS"
{ echo 'Include ~/mac/config/ssh_homelab'; [[ -f $HOME/.ssh/config ]] && cat "$HOME/.ssh/config"; } > "$HOME/.ssh/config.new" { echo 'Include ~/mac/config/ssh_homelab'; [[ -f $HOME/.ssh/config ]] && cat "$HOME/.ssh/config"; } > "$HOME/.ssh/config.new"