diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a020af..cef1a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ ## Changelog +### v0.106.1 — offsite apply-bridge: ssh-copy-id -s needs ~/.ssh to exist (live finding F3) (2026-07-09) + +First supervised live apply: scan+verify passed, the one-time password was consumed, then `ssh-copy-id -s` +died **locally** — SFTP mode mktemp's its batch file under `~/.ssh`, and the container image ships without +`/root/.ssh`. The fail-safe held (loud "password is spent" signal, no marker, no offbox config) and the +password never left the box, but the install could never succeed. + +- `internal/offsiteapply.SSHCopyIDInstaller`: ensure `~/.ssh` (0700) exists before running `ssh-copy-id`. +- Live diagnosis (container, no secrets): with `~/.ssh` present, the pinned single-line known_hosts + + `StrictHostKeyChecking=yes` verifies cleanly and a wrong password fails as `Permission denied` (sshpass + exit 5) — the TOCTOU-hardened pin mechanics are sound end-to-end against the real box. + ### v0.106.0 — offsite provisioning SLICE 2: controller apply-bridge (2026-07-09) Pairs with hub v0.38.0. On startup the controller reconciles the hub-served `offsite:` descriptor into a diff --git a/controller/internal/offsiteapply/seams.go b/controller/internal/offsiteapply/seams.go index 4064d3b..8760b94 100644 --- a/controller/internal/offsiteapply/seams.go +++ b/controller/internal/offsiteapply/seams.go @@ -146,6 +146,14 @@ func (SSHCopyIDInstaller) Install(ctx context.Context, host, user string, port i if strings.TrimSpace(knownHosts) == "" { return fmt.Errorf("ssh-copy-id: empty known_hosts — refusing to install without a pinned host key") } + // ssh-copy-id -s (SFTP mode) mktemp's its batch file under ~/.ssh and dies LOCALLY if the directory + // doesn't exist — the container image ships without /root/.ssh (live finding: the one-time password was + // consumed, then the install failed before ever connecting). + if home, err := os.UserHomeDir(); err == nil { + if err := os.MkdirAll(filepath.Join(home, ".ssh"), 0o700); err != nil { + return fmt.Errorf("ssh-copy-id: ensure ~/.ssh (needed by -s mode): %w", err) + } + } work, err := os.MkdirTemp("", "felhom-keyinstall-") if err != nil { return err