Spike de validación (sucio, temporal) que cierra el agujero que el bwrap anidado dejó: bootea un work/rootfs-ssh-test desechable (4/4 limpio del store + openssh + netup PRECOMPILADOS) como initramfs, con sshd como genesis card supervisado por arje-zero, red slirp con hostfwd :2222->:22, y hace el handshake SSH real desde el host. VERDE: arje valida la seed e instancia el card sshd (Native/Restart); netup toma lease 10.0.2.15 (DHCP slirp); ssh-keygen -A genera host keys; sshd escucha :22; "Accepted publickey for root" y el guest ejecuta el comando (HANDSHAKE_OK uid=0, Linux 6.16.12). Quema las 3 incógnitas: (1) handshake post-chroot/setuid de privsep como root REAL del guest (lo que el bwrap no pudo), (2) arje gobierna el lifecycle del daemon, (3) los fds de red no segfaultean contra musl. Gotchas: cpio -R 0:0 (sin root-owned /var/empty sshd lo rechaza); chmod u+w sobre el staging (el store está sellado read-only). NO es plomería de producto: la consolidación (SERVICE_COMPONENTS + hidratacion tardia sobre el 4/4 verificado, of_tree blindado) es el Paso 2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
194 lines
9.5 KiB
Bash
Executable File
194 lines
9.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ssh-e2e-test.sh — SPIKE de validación (sucio, temporal) del Paso 1: probar el handshake SSH REAL
|
|
# contra un hammer booteado en QEMU, con sshd supervisado por arje-zero como genesis card.
|
|
#
|
|
# Esto NO es plomería de producto: arma un `work/rootfs-ssh-test` desechable = el 4/4 limpio del
|
|
# store + openssh + netup (todos PRECOMPILADOS de la caché del lab, no rebuild) + una seed con un
|
|
# card `sshd`, y lo bootea como initramfs. La red es QEMU slirp con hostfwd :2222->:22, así que el
|
|
# host hace `ssh -p 2222 root@localhost` SIN tocar firewall/bridge (la lección de netup). El chroot
|
|
# de privsep —que el bwrap anidado no pudo— acá corre como root REAL del guest.
|
|
#
|
|
# Quema tres incógnitas que el sandbox de bwrap dejó abiertas:
|
|
# 1. el handshake completo (post-chroot, post-setuid sshd-user) ocurre,
|
|
# 2. arje maneja el ciclo de vida del daemon (Restart) como cualquier card Native,
|
|
# 3. los fds de red de sshd no segfaultean contra el musl del target.
|
|
#
|
|
# Variables: KERNEL (def store/*-linux/boot/bzImage) MEM (def 2048) KVM (1 si /dev/kvm)
|
|
# PORT (hostfwd, def 2222) DEADLINE (def 150s)
|
|
set -euo pipefail
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
KERNEL="${KERNEL:-$(ls store/*-linux/boot/bzImage 2>/dev/null | head -1)}"
|
|
MEM="${MEM:-2048}"
|
|
KVM="${KVM:-1}"
|
|
PORT="${PORT:-2222}"
|
|
DEADLINE="${DEADLINE:-150}"
|
|
WORK="$ROOT/work/ssh-e2e"
|
|
RFS="$ROOT/work/rootfs-ssh-test"
|
|
LOG="$WORK/console.log"
|
|
|
|
pick() { ls -d store/*-"$1" 2>/dev/null | head -1; }
|
|
MUSL=$(pick musl); BB=$(pick busybox); ARJE="store/69e2cab8ac00c33cde7680eb393952e2c7f9ae9af24c180c6593d7a86482efb9-arje-zero"
|
|
NETUP=$(pick netup); OSSH=$(pick openssh)
|
|
for v in KERNEL MUSL BB ARJE NETUP OSSH; do
|
|
p="${!v}"; [ -n "$p" ] && [ -e "$p" ] || { echo "falta $v ($p)"; exit 1; }
|
|
done
|
|
echo "==> kernel $KERNEL"
|
|
echo "==> componentes: musl busybox arje-zero netup openssh (precompilados del store)"
|
|
|
|
# ── 1) ensamblar el rootfs desechable ───────────────────────────────────────────────────────────
|
|
rm -rf "$RFS" "$WORK"; mkdir -p "$RFS" "$WORK"
|
|
for src in "$MUSL" "$BB" "$ARJE" "$NETUP" "$OSSH"; do
|
|
cp -a "$src"/. "$RFS"/ 2>/dev/null || true
|
|
done
|
|
chmod -R u+w "$RFS" # el store está sellado read-only; el staging debe ser escribible
|
|
rm -rf "$RFS/.hammer"
|
|
mkdir -p "$RFS"/{proc,sys,dev,dev/pts,run,tmp,etc,etc/ssh,root,root/.ssh,var,var/empty,ente,sbin,bin,usr/bin}
|
|
chmod 0711 "$RFS/var/empty"
|
|
ln -sf /usr/bin/arje-zero "$RFS/sbin/init"
|
|
# busybox a /bin/sh + applets que la getty/seed necesitan (por si el cp no dejó symlinks en /bin)
|
|
[ -e "$RFS/bin/busybox" ] || cp -a "$BB"/bin/busybox "$RFS/bin/busybox"
|
|
for a in sh getty; do ln -sf busybox "$RFS/bin/$a"; done
|
|
|
|
# usuarios: root (login por llave) + sshd (privsep). shells reales para que sshd ejecute el comando.
|
|
cat > "$RFS/etc/passwd" <<'EOF'
|
|
root:x:0:0:root:/root:/bin/sh
|
|
sshd:x:74:74:sshd privsep:/var/empty:/bin/false
|
|
EOF
|
|
cat > "$RFS/etc/group" <<'EOF'
|
|
root:x:0:
|
|
sshd:x:74:
|
|
EOF
|
|
cat > "$RFS/etc/ssh/sshd_config" <<'EOF'
|
|
Port 22
|
|
ListenAddress 0.0.0.0
|
|
PermitRootLogin yes
|
|
PubkeyAuthentication yes
|
|
PasswordAuthentication no
|
|
AuthorizedKeysFile /root/.ssh/authorized_keys
|
|
StrictModes no
|
|
Subsystem sftp /usr/libexec/sftp-server
|
|
EOF
|
|
|
|
# ── 2) llave de cliente desechable (host) → authorized_keys del guest ────────────────────────────
|
|
"$OSSH/usr/bin/ssh-keygen" -t ed25519 -N '' -f "$WORK/clientkey" -q
|
|
cat "$WORK/clientkey.pub" > "$RFS/root/.ssh/authorized_keys"
|
|
chmod 0600 "$RFS/root/.ssh/authorized_keys"
|
|
|
|
# ── 3) seed card: console-getty + un card `sshd` (Native, Restart) ───────────────────────────────
|
|
# El card sshd arranca la red (netup, DHCP slirp), genera host keys (ssh-keygen -A) y exec sshd -D.
|
|
# Mismo esquema validado de STAGE1_SEED_CARD (hammer-bootstrap); ULIDs nuevos A10/A11/A12.
|
|
SSHD_CMD='/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e'
|
|
cat > "$RFS/ente/seed.card.json" <<EOF
|
|
{
|
|
"schema_version": 1,
|
|
"id": "01HQAR53D4M2NBV8KZTYXFQA10",
|
|
"lineage": null,
|
|
"label": "hammer-ssh-spike",
|
|
"provides": ["Spawn", "Journal"],
|
|
"requires": [],
|
|
"permissions": { "networking": "full", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
|
|
"soma": {
|
|
"namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
|
|
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
|
|
"cgroup": { "path": "arje.slice/hammer", "cpu_weight": null, "io_weight": null },
|
|
"cpu_affinity": null
|
|
},
|
|
"payload": "Virtual",
|
|
"supervision": "OneShot",
|
|
"lifecycle": "daemon",
|
|
"priority": "normal",
|
|
"flow": { "input": [], "output": [] },
|
|
"genesis": [
|
|
{
|
|
"schema_version": 1,
|
|
"id": "01HQAR53D4M2NBV8KZTYXFQA11",
|
|
"lineage": null,
|
|
"label": "console-getty",
|
|
"provides": [],
|
|
"requires": [],
|
|
"permissions": { "networking": "none", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
|
|
"soma": {
|
|
"namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
|
|
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
|
|
"cgroup": { "path": "arje.slice/getty", "cpu_weight": null, "io_weight": null },
|
|
"cpu_affinity": null
|
|
},
|
|
"payload": { "Native": { "exec": "/bin/busybox", "argv": ["getty", "-n", "-l", "/bin/sh", "115200", "console"], "envp": [] }},
|
|
"supervision": { "Restart": { "initial": 200, "max": 10000 } },
|
|
"lifecycle": "daemon",
|
|
"priority": "normal",
|
|
"flow": { "input": [], "output": [] },
|
|
"genesis": []
|
|
},
|
|
{
|
|
"schema_version": 1,
|
|
"id": "01HQAR53D4M2NBV8KZTYXFQA12",
|
|
"lineage": null,
|
|
"label": "sshd",
|
|
"provides": [],
|
|
"requires": [],
|
|
"permissions": { "networking": "full", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
|
|
"soma": {
|
|
"namespaces": { "mount": false, "pid": false, "net": false, "uts": false, "ipc": false, "user": false, "cgroup": false },
|
|
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
|
|
"cgroup": { "path": "arje.slice/sshd", "cpu_weight": null, "io_weight": null },
|
|
"cpu_affinity": null
|
|
},
|
|
"payload": { "Native": { "exec": "/bin/busybox", "argv": ["sh", "-c", "${SSHD_CMD}"], "envp": [["HOME", "/root"]] }},
|
|
"supervision": { "Restart": { "initial": 500, "max": 20000 } },
|
|
"lifecycle": "daemon",
|
|
"priority": "normal",
|
|
"flow": { "input": [], "output": [] },
|
|
"genesis": []
|
|
}
|
|
]
|
|
}
|
|
EOF
|
|
|
|
# ── 4) empaquetar initramfs ──────────────────────────────────────────────────────────────────────
|
|
# -R 0:0: el cpio corre como uid no-root; sin esto el rootfs queda con nuestro uid y sshd rechaza
|
|
# /var/empty ("must be owned by root"). Reasigna todo a root:root dentro del archivo.
|
|
( cd "$RFS" && find . -print0 | cpio --null -o -H newc -R 0:0 2>/dev/null | gzip -1 ) > "$WORK/initramfs.cpio.gz"
|
|
echo "==> initramfs $(du -h "$WORK/initramfs.cpio.gz" | cut -f1)"
|
|
|
|
# ── 5) boot QEMU (slirp + hostfwd) en background ─────────────────────────────────────────────────
|
|
accel=(-cpu Broadwell)
|
|
if [ "$KVM" = 1 ] && [ -w /dev/kvm ]; then accel=(-enable-kvm -cpu host); fi
|
|
echo "==> boot QEMU (mem ${MEM}, kvm $([ "${accel[0]}" = -enable-kvm ] && echo sí || echo no)), hostfwd :$PORT->:22"
|
|
qemu-system-x86_64 -m "$MEM" -no-reboot -nographic "${accel[@]}" \
|
|
-kernel "$KERNEL" -initrd "$WORK/initramfs.cpio.gz" \
|
|
-append "console=ttyS0 rdinit=/sbin/init" \
|
|
-netdev "user,id=n0,hostfwd=tcp::$PORT-:22" -device e1000,netdev=n0 \
|
|
> "$LOG" 2>&1 &
|
|
QPID=$!
|
|
trap 'kill $QPID 2>/dev/null || true' EXIT
|
|
|
|
# ── 6) esperar a sshd y hacer el handshake real ──────────────────────────────────────────────────
|
|
SSHOPTS=(-p "$PORT" -i "$WORK/clientkey" -o StrictHostKeyChecking=no
|
|
-o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null
|
|
-o PasswordAuthentication=no -o ConnectTimeout=4 -o LogLevel=ERROR)
|
|
echo "==> esperando handshake (deadline ${DEADLINE}s)…"
|
|
start=$(date +%s); ok=0
|
|
while [ $(( $(date +%s) - start )) -lt "$DEADLINE" ]; do
|
|
if ! kill -0 $QPID 2>/dev/null; then echo "!! QEMU murió"; break; fi
|
|
if out=$(ssh "${SSHOPTS[@]}" root@localhost \
|
|
'echo HANDSHAKE_OK uid=$(id -u) user=$(id -un); uname -srm; cat /etc/os-release 2>/dev/null | head -1' 2>/dev/null); then
|
|
echo "================ RESPUESTA DEL GUEST ================"
|
|
echo "$out"
|
|
echo "===================================================="
|
|
echo "$out" | grep -q HANDSHAKE_OK && ok=1
|
|
break
|
|
fi
|
|
sleep 3
|
|
done
|
|
|
|
# ── 7) veredicto + diagnóstico ───────────────────────────────────────────────────────────────────
|
|
echo "---- consola (cola) ----"; tail -25 "$LOG" 2>/dev/null
|
|
if [ "$ok" = 1 ]; then
|
|
echo; echo "*** SSH E2E OK — handshake real contra hammer+arje, sshd como card ***"; exit 0
|
|
else
|
|
echo; echo "*** SSH E2E FALLÓ — ver $LOG ***"; exit 1
|
|
fi
|