Skip to content

sshd: implement --ssh.enable with a pure-Go OpenSSH server#6

Merged
ananthb merged 2 commits into
mainfrom
feat/ssh-server
Jul 12, 2026
Merged

sshd: implement --ssh.enable with a pure-Go OpenSSH server#6
ananthb merged 2 commits into
mainfrom
feat/ssh-server

Conversation

@ananthb

@ananthb ananthb commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • --ssh.enable was a stub (slog.Info(\"ssh requested\")), so rescue sessions couldn't actually connect. Now it runs a pure-Go OpenSSH server (golang.org/x/crypto/ssh + github.com/creack/pty) inside the pivoted rootfs.
  • Auth: public keys from --ssh.authorized-keys (standard authorized_keys format, one per line; blank + comment lines skipped) OR password from --ssh.password. Both accepted when both set; server refuses to start with neither.
  • Sessions: pty-req + shell → interactive /bin/sh -l in a real pty (creack/pty); exec → non-pty /bin/sh -c \"…\". TERM, env requests, window-change all propagated.
  • Ephemeral ed25519 host key generated at start; fingerprint logged. All sessions run as root — the pivoted rootfs has no user db.
  • Dry-run output now shows the SSH step + configured auth methods.

Linux-only; non-Linux stub returns "not supported" (pty + os/exec shell handling needs Linux semantics).

Motivation

Ran a real pivot into a coreos-installer image today. The tsnet node registered on the tailnet but SSH timed out — tsnet doesn't ship the Tailscale SSH server, and xmorph's own --ssh.enable was a no-op. No way to observe the install; had to power-cycle when things went sideways.

Test plan

  • go test ./internal/postpivot/... ./internal/config/... ./internal/cli/... on Linux (endeavour): all pass.
  • New tests cover: pubkey exec (echo hello), exit status propagation, password auth, unknown-key rejection, missing-auth refusal, multi-line authorized_keys parsing (with blank + comment lines), bad-line error surfacing.
  • Real hardware: retry the enterprise Flatcar pivot with --ssh.enable --ssh.authorized-keys ...; confirm we can SSH into the pivoted env and observe install progress.

Follow-ups (out of scope)

  1. --ssh.tailscale-auth: bind on the tsnet listener and use LocalClient().WhoIs() to identify the peer, trust any tailnet member (no ACL parsing yet).
  2. Full tailnet SSH-policy enforcement — duplicates a big chunk of tailscaled; only worth it if someone actually needs it.
  3. Optional persistent host key (write to /etc/xmorph/ssh_host_ed25519_key) so known_hosts entries survive multi-pivot fleets.

ananthb added 2 commits July 13, 2026 02:53
The M4 stub only logged "ssh requested" — no server, so rescue
sessions couldn't connect. Now `--ssh.enable` runs a pure-Go OpenSSH
server (golang.org/x/crypto/ssh + github.com/creack/pty) inside the
pivoted rootfs.

- Ephemeral ed25519 host key; fingerprint logged.
- Auth: public keys parsed from --ssh.authorized-keys (standard
  authorized_keys format, one per line; blank + comment lines
  skipped) OR password from --ssh.password. Both accepted when both
  set. Server refuses to start with neither configured.
- Sessions: pty for interactive shell (pty-req + shell), plain pipes
  for exec. TERM, env vars, and window resizes propagated.
- All sessions run /bin/sh as root — the rescue rootfs has no user db.

Linux-only; non-Linux stub returns "not supported."

Follow-ups (separate PRs): --ssh.tailscale-auth to bind on the tsnet
listener + WhoIs-check tailnet peer identity (no ACL parsing yet).
@ananthb
ananthb merged commit 559671c into main Jul 12, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant