feat(seed): outbound SSH client material for hands-free node->mother
The baked mother key (build/mother-ssh-key) puts a private key in the image, which only works for a non-published personalized stick. The offline FAT32 seed is the correct home for per-node secrets. Teach the importer to install outbound SSH client material from an agent's ssh/ dir into the agent home: - config -> ~/.ssh/config (0600) - known_hosts* -> ~/.ssh/known_hosts* (0644, merged + de-duped) - <name>.pub -> ~/.ssh/<name>.pub (0644) - <name> -> ~/.ssh/<name> (0600, any other file = private key) authorized_keys stays inbound-only via _seed_install_authorized_keys. This closes the 'without manual key exchange' gap: known_hosts pins mother's host key so the first node->mother connect does not prompt, and the private client key rides on the offline seed instead of the base image — so the published image stays secret-free. Supersedes the baked-key path (#112), which can retire once this is validated on hardware. Verified offline (CLAWDIE_SEED_TEST): correct perms (key 0600, pub/known_hosts 0644, config 0600, .ssh 0700) and idempotent known_hosts merge across re-runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
72491ee3b8
commit
862af0583b
2 changed files with 91 additions and 1 deletions
|
|
@ -112,6 +112,65 @@ _seed_install_authorized_keys() {
|
||||||
_seed_log "installed authorized_keys from ${_src} -> ${_dst}"
|
_seed_log "installed authorized_keys from ${_src} -> ${_dst}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Install OUTBOUND ssh client material from an agent's ssh/ dir into the agent
|
||||||
|
# home so a seeded node can SSH out (e.g. node -> mother) hands-free, with the
|
||||||
|
# private key delivered on the offline seed instead of baked into the image:
|
||||||
|
# config -> ~/.ssh/config (0600; e.g. a "Host mother" alias)
|
||||||
|
# known_hosts* -> ~/.ssh/known_hosts* (0644; merged, so mother's host key
|
||||||
|
# is trusted and the first connect
|
||||||
|
# does not prompt)
|
||||||
|
# <name>.pub -> ~/.ssh/<name>.pub (0644)
|
||||||
|
# <name> -> ~/.ssh/<name> (0600; any other file = private key)
|
||||||
|
# authorized_keys is INBOUND and handled by _seed_install_authorized_keys.
|
||||||
|
_seed_install_ssh_material() {
|
||||||
|
_srcdir="$1"
|
||||||
|
_user="${2:-${SEED_USER}}"
|
||||||
|
_home="${3:-${SEED_USER_HOME}}"
|
||||||
|
|
||||||
|
[ -d "${_srcdir}" ] || return 0
|
||||||
|
|
||||||
|
_ssh_dir="${_home}/.ssh"
|
||||||
|
mkdir -p "${_ssh_dir}"
|
||||||
|
chown "${_user}:${_user}" "${_ssh_dir}" 2>/dev/null || true
|
||||||
|
chmod 0700 "${_ssh_dir}"
|
||||||
|
|
||||||
|
for _f in "${_srcdir}"/*; do
|
||||||
|
[ -f "${_f}" ] || continue
|
||||||
|
_base="$(basename "${_f}")"
|
||||||
|
_dst="${_ssh_dir}/${_base}"
|
||||||
|
case "${_base}" in
|
||||||
|
authorized_keys)
|
||||||
|
continue ;; # inbound, installed separately
|
||||||
|
known_hosts|known_hosts2)
|
||||||
|
touch "${_dst}"
|
||||||
|
tr -d '\r' <"${_f}" >>"${_dst}"
|
||||||
|
# De-dup so re-imports stay idempotent (order is irrelevant here).
|
||||||
|
sort -u "${_dst}" -o "${_dst}" 2>/dev/null || true
|
||||||
|
chmod 0644 "${_dst}"
|
||||||
|
chown "${_user}:${_user}" "${_dst}" 2>/dev/null || true
|
||||||
|
_seed_log "merged ${_base} -> ${_dst}" ;;
|
||||||
|
config)
|
||||||
|
tr -d '\r' <"${_f}" >"${_dst}.new"
|
||||||
|
mv -f "${_dst}.new" "${_dst}"
|
||||||
|
chmod 0600 "${_dst}"
|
||||||
|
chown "${_user}:${_user}" "${_dst}" 2>/dev/null || true
|
||||||
|
_seed_log "installed ssh config -> ${_dst}" ;;
|
||||||
|
*.pub)
|
||||||
|
tr -d '\r' <"${_f}" >"${_dst}.new"
|
||||||
|
mv -f "${_dst}.new" "${_dst}"
|
||||||
|
chmod 0644 "${_dst}"
|
||||||
|
chown "${_user}:${_user}" "${_dst}" 2>/dev/null || true
|
||||||
|
_seed_log "installed public key -> ${_dst}" ;;
|
||||||
|
*)
|
||||||
|
tr -d '\r' <"${_f}" >"${_dst}.new"
|
||||||
|
mv -f "${_dst}.new" "${_dst}"
|
||||||
|
chmod 0600 "${_dst}"
|
||||||
|
chown "${_user}:${_user}" "${_dst}" 2>/dev/null || true
|
||||||
|
_seed_log "installed private key -> ${_dst}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Valid POSIX-ish shell env var name. Guards against garbled seed lines
|
# Valid POSIX-ish shell env var name. Guards against garbled seed lines
|
||||||
# injecting odd content into .env.
|
# injecting odd content into .env.
|
||||||
_seed_key_ok() {
|
_seed_key_ok() {
|
||||||
|
|
@ -285,6 +344,11 @@ _seed_activate_agent() {
|
||||||
if [ -f "${_dir}/ssh/authorized_keys" ]; then
|
if [ -f "${_dir}/ssh/authorized_keys" ]; then
|
||||||
_seed_install_authorized_keys "${_dir}/ssh/authorized_keys" "${_user}" "${_home}"
|
_seed_install_authorized_keys "${_dir}/ssh/authorized_keys" "${_user}" "${_home}"
|
||||||
fi
|
fi
|
||||||
|
# Outbound client material (config, known_hosts, client keys) for node->mother
|
||||||
|
# connectivity, delivered via the offline seed rather than baked in the image.
|
||||||
|
if [ -d "${_dir}/ssh" ]; then
|
||||||
|
_seed_install_ssh_material "${_dir}/ssh" "${_user}" "${_home}"
|
||||||
|
fi
|
||||||
|
|
||||||
_seed_log "activated agent '${_agent}' for user ${_user} (home ${_home})"
|
_seed_log "activated agent '${_agent}' for user ${_user} (home ${_home})"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,33 @@ Inside it, any of these are honored:
|
||||||
under /var/db/clawdie/seed/<agent>/soul for
|
under /var/db/clawdie/seed/<agent>/soul for
|
||||||
the agent workspace to load.
|
the agent workspace to load.
|
||||||
|
|
||||||
/<agent>/ssh/authorized_keys Public SSH keys for this agent.
|
/<agent>/ssh/authorized_keys INBOUND: public keys allowed to SSH INTO this
|
||||||
|
node. Installed to ~/.ssh/authorized_keys (0600).
|
||||||
|
|
||||||
|
/<agent>/ssh/<name> OUTBOUND: a private client key so this node can
|
||||||
|
SSH OUT hands-free (e.g. node -> mother).
|
||||||
|
Installed to ~/.ssh/<name> (0600). The private
|
||||||
|
key rides on this offline seed instead of being
|
||||||
|
baked into the image, so the image stays
|
||||||
|
secret-free. Any file here that is not
|
||||||
|
authorized_keys/config/known_hosts and does not
|
||||||
|
end in .pub is treated as a private key.
|
||||||
|
|
||||||
|
/<agent>/ssh/<name>.pub OUTBOUND: installed to ~/.ssh/<name>.pub (0644).
|
||||||
|
|
||||||
|
/<agent>/ssh/config OUTBOUND: installed to ~/.ssh/config (0600).
|
||||||
|
Typical use — a host alias for the mother server:
|
||||||
|
|
||||||
|
Host mother
|
||||||
|
HostName osa.smilepowered.org
|
||||||
|
User clawdie
|
||||||
|
IdentityFile ~/.ssh/osa-mother-2026
|
||||||
|
|
||||||
|
/<agent>/ssh/known_hosts OUTBOUND: merged into ~/.ssh/known_hosts (0644),
|
||||||
|
de-duplicated. Pin the mother server's host key
|
||||||
|
here so the first node -> mother connection does
|
||||||
|
not stop on an unknown-host prompt. Get the line
|
||||||
|
with: ssh-keyscan osa.smilepowered.org
|
||||||
|
|
||||||
Agent directory names may contain only A-Z a-z 0-9 . _ - (no spaces or
|
Agent directory names may contain only A-Z a-z 0-9 . _ - (no spaces or
|
||||||
slashes). The name `ssh` is reserved for Layer 1.
|
slashes). The name `ssh` is reserved for Layer 1.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue