fix(firecracker): make the launch path work end-to-end
lint / lint (push) Failing after 2m2s
test / unit (pull_request) Successful in 56s
test / integration (pull_request) Successful in 24s
test / coverage (pull_request) Failing after 1m11s

First real end-to-end launch (there was no firecracker integration test)
surfaced three bugs in the control/provision path, all now verified fixed
by tests/integration/test_firecracker_launch:

1. Guest SSH rejected the correct key. The rootless rootfs build
   (`docker export | tar` as a non-root user) can't preserve uid 0, so
   every path — including /root — is owned by the build uid (node in
   guest). dropbear refuses root's authorized_keys when /root isn't
   root-owned, so auth fell back to password → denied. bb-init now
   `chown -R 0:0 /root`.

2. The SSH client (newer OpenSSH) didn't reliably present the -i key
   against the operator's ~/.ssh config; add `IdentitiesOnly=yes` to
   ssh_base_argv so only the per-bottle key is offered.

3. cp_in double-wrapped the remote command as `sh -c <remote>`, but ssh
   space-joins everything after the host into one string for the guest
   shell, collapsing `sh -c mkdir -p X && …` to `mkdir` with no operand
   ("missing operand"). Pass the command as a single arg and let the
   guest login shell run it (stdin still carries the tar).

Also stop discarding dropbear's stderr (`-E` now reaches the host-side
console.log) so future guest-auth issues are debuggable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
2026-07-11 18:16:57 -04:00
parent f42a0dc7fe
commit 3d7c508dc4
2 changed files with 20 additions and 3 deletions
+6 -1
View File
@@ -151,8 +151,13 @@ class FirecrackerBottle(Bottle):
["tar", "-C", host_parent, "-cf", "-", host_base], ["tar", "-C", host_parent, "-cf", "-", host_base],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
) )
# Pass the command as one arg: ssh space-joins everything after
# the host into a single string for the guest's login shell, so a
# `sh -c <remote>` split would drop everything past the first word
# (the guest shell runs `<remote>` directly; stdin carries the
# tar).
ssh = subprocess.run( ssh = subprocess.run(
[*self._ssh(tty=False), "--", "sh", "-c", remote], [*self._ssh(tty=False), "--", remote],
stdin=tar.stdout, capture_output=True, text=True, check=False, stdin=tar.stdout, capture_output=True, text=True, check=False,
) )
tar.wait() tar.wait()
+14 -2
View File
@@ -268,6 +268,10 @@ def ssh_base_argv(private_key: Path, guest_ip: str) -> list[str]:
return [ return [
"ssh", "ssh",
"-i", str(private_key), "-i", str(private_key),
# Only offer the per-bottle key — don't let an agent or the
# operator's ~/.ssh/config inject other identities (newer
# OpenSSH otherwise may not reliably present the -i key).
"-o", "IdentitiesOnly=yes",
"-o", "StrictHostKeyChecking=no", "-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null", "-o", "UserKnownHostsFile=/dev/null",
"-o", "LogLevel=ERROR", "-o", "LogLevel=ERROR",
@@ -301,9 +305,17 @@ if [ -n "$KEY" ]; then
chown -R node:node /home/node/.ssh 2>/dev/null || true chown -R node:node /home/node/.ssh 2>/dev/null || true
fi fi
# The rootless rootfs build (`docker export | tar` as a non-root user)
# can't preserve uid 0, so every path lands owned by the build uid,
# which maps to `node` (uid 1000) in-guest — including /root. dropbear
# (like OpenSSH) refuses root's authorized_keys unless the home dir is
# owned by root, so restore root's ownership of its own home.
chown -R 0:0 /root 2>/dev/null || true
mkdir -p /etc/dropbear /run mkdir -p /etc/dropbear /run
# -R: generate host keys on demand. -E: log to stderr (guest console). # -R: generate host keys on demand. -E: log auth failures to stderr,
/bb-dropbear -R -E -p 22 2>/dev/null & # captured in the host-side console.log for debugging.
/bb-dropbear -R -E -p 22 &
# Reap zombies as PID 1. dropbear is always a child, so `wait` blocks # Reap zombies as PID 1. dropbear is always a child, so `wait` blocks
# rather than busy-looping. # rather than busy-looping.