diff --git a/CHANGELOG.md b/CHANGELOG.md index 763c5ce..bbf16b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,22 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a ## Unreleased -_(no changes since v1.15.13d)_ +### Added: persist the LAN-jump key + one-line authorize hint (authorize once per machine) + +The jump keypair (`~/.ssh-local/devbox_jump_ed25519`) was stored on the +container's ephemeral overlay, so `docker compose up --force-recreate` (every +image update) regenerated it — forcing you to re-authorize the new key on the +host each time. The compose files now persist `~/.ssh-local` via a named volume +(`devbox-ssh-local`), matching the pattern already used for `.pi`, shell +history, etc. The key is generated **once** and reused across updates, so you +authorize it on the host **once per machine**. + +`setup-lan-access.sh` now also prints a ready-to-paste authorize line whenever +it generates a **new** key (not just when `HOST_SSH_USER` is unset), e.g. +`echo 'ssh-ed25519 …' >> ~/.ssh/authorized_keys` — no helper file to locate, no +workspace path to guess. It stays silent once the key is persisted. + +_(no other changes since v1.15.13d)_ ## v1.15.13d — 2026-06-04 diff --git a/README.md b/README.md index 12612ea..dd43c1c 100644 --- a/README.md +++ b/README.md @@ -155,15 +155,20 @@ The devbox works the same way whether the host is **native Linux Docker** or a * - **Native Linux Docker:** the host NATs container egress onto its LAN, so other devices on your LAN are reachable directly. Nothing to configure. - **VM-backed (macOS / Docker Desktop):** the container runs in a Linux VM behind the host's network stack. The host's *directly-attached* LAN peers are **not** bridged into the container by default — only the host itself and *routed* subnets are reachable. -On every start the entrypoint detects which case applies. On VM-backed hosts it generates a writable `~/.ssh-local/config` that uses the **host as an SSH jump** to reach LAN peers; on native Linux it does nothing. +On every start the entrypoint detects which case applies. On VM-backed hosts it generates a writable `~/.ssh-local/config` that uses the **host as an SSH jump** to reach LAN peers; on native Linux it does nothing. The jump keypair lives in `~/.ssh-local`, which is persisted by the `devbox-ssh-local` named volume — so it's generated **once** and reused across container updates. -**To enable it on a VM-backed host:** +**To enable it on a VM-backed host (one-time setup per machine):** 1. Set `HOST_SSH_USER=` in `.env`. -2. Start the container once. The entrypoint prints a public key — append it to your host's `~/.ssh/authorized_keys`. +2. Start the container once. When it generates the jump key it prints a ready-to-paste line — run it **on the host** to authorize the key: + ```bash + echo 'ssh-ed25519 AAAA…devbox-jump@…' >> ~/.ssh/authorized_keys + ``` 3. Ensure the host's SSH server is on (on macOS: System Settings → General → Sharing → Remote Login). 4. Reach the host itself with `dssh host`. (`dssh`/`dscp` wrap `ssh -F ~/.ssh-local/config`.) +Because the key is persisted, you do this **once per machine** — not after every `docker compose up --force-recreate`. You'll only see the authorize line again if you reset the `devbox-ssh-local` volume. + That alone gets you `container → host`. To reach **named LAN peers** by name, give them a `ProxyJump host` override. Don't add it to the shared `~/.ssh/config` entries — the host itself reaches those peers *directly*, and a jump-through-`host` would break the host's own access (and that file is mounted read-only anyway). Instead, drop the overrides in a **host-owned** file that the container Includes ahead of your `~/.ssh/config`: ```sshconfig diff --git a/docker-compose.yml b/docker-compose.yml index 4505208..e603d14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,6 +59,11 @@ services: # allowing both native and containerized opencode on the same machine. - devbox-opencode-config:/home/developer/.config/opencode - devbox-pi-config:/home/developer/.pi + # Persist the generated LAN-jump keypair (~/.ssh-local) across recreates. + # setup-lan-access.sh generates this key once and reuses it; persisting + # it means you authorize it on the host ONCE rather than re-authorizing + # after every `docker compose up --force-recreate`. + - devbox-ssh-local:/home/developer/.ssh-local # NOTE: Do NOT bind-mount ~/.agents/skills/ from the host. The # container manages its own skills directory independently — the @@ -134,6 +139,7 @@ services: volumes: devbox-opencode-config: devbox-pi-config: + devbox-ssh-local: devbox-data: devbox-state: devbox-shell-history: diff --git a/rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh b/rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh index 83ba9ae..5061a9a 100755 --- a/rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh +++ b/rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh @@ -97,9 +97,15 @@ mkdir -p "${SSH_LOCAL}/cm" 2>/dev/null || true chmod 700 "${SSH_LOCAL}" "${SSH_LOCAL}/cm" 2>/dev/null || true # ── Jump key (generated once; preserved across restarts) ────────────── +# Persisted via a named volume on ~/.ssh-local (see compose), so a fresh key +# is generated only on the very first start (or if the volume is wiped). When +# we DO generate one it must be (re-)authorized on the host, so we flag it and +# print a copy-paste authorize line below. +KEY_JUST_GENERATED=0 if [ ! -f "$KEY" ]; then ssh-keygen -t ed25519 -N '' -C "devbox-jump@${HOSTNAME:-container}" -f "$KEY" >/dev/null 2>&1 || exit 0 chmod 600 "$KEY" 2>/dev/null || true + KEY_JUST_GENERATED=1 fi # ── Render the writable config ──────────────────────────────────────── @@ -187,18 +193,33 @@ ${INCLUDE_BLOCK} EOF chmod 600 "$CONFIG" 2>/dev/null || true -# ── One-time hint when we can't authenticate yet ────────────────────── +# ── Authorize hints ─────────────────────────────────────────────────── +# Print the copy-paste authorize line whenever we either (a) can't yet +# authenticate (HOST_SSH_USER unset) or (b) just generated a NEW key that the +# host won't recognize. With ~/.ssh-local persisted via a named volume, case +# (b) fires only on first-ever start (or after the volume is reset) — so this +# is normally a one-time, one-line step per machine, with no file to locate. +PUBKEY_TEXT="$(cat "${KEY}.pub" 2>/dev/null)" if [ -z "${HOST_SSH_USER:-}" ]; then cat < host -> LAN-peer access: 1. Set HOST_SSH_USER= in the container env. - 2. Authorize this key on the host (append to ~/.ssh/authorized_keys): - $(cat "${KEY}.pub" 2>/dev/null) + 2. Authorize this key on the host (run ON THE HOST, once): + echo '${PUBKEY_TEXT}' >> ~/.ssh/authorized_keys 3. Ensure the host's SSH server (Remote Login) is enabled. Then: dssh host (or add 'ProxyJump host' to targets in ~/.ssh/config) EOF +elif [ "$KEY_JUST_GENERATED" = "1" ]; then + cat <> ~/.ssh/authorized_keys + (Ensure the host's SSH server / Remote Login is enabled.) + This key is persisted in the ~/.ssh-local volume, so you won't need to + repeat this on container updates — only if that volume is reset. +EOF fi exit 0