Overview
Make the lilbox gateway work over Tailscale SSH with the caller's tailnet identity — no SSH keys, one local user, per-VM ownership in-app. End users install nothing: on the tailnet + ACL allows → ssh lilbox@host new --tailscale mybox just works.
De-risked in the spike #109 (findings there). Extends the gateway from #108 (key-based OpenSSH forced-command MVP).
Why an epic (not a sprint)
The children interlock on one runtime contract: the -c entry refactor gates identity resolution, identity gates ownership authz, both gate a meaningful install, and hardening/docs describe the assembled whole. Any one alone on main would ship a half-wired gateway — so they stack on a shared integration branch and squash to trunk once.
Base: the integration branch is cut from paulg/gateway-ssh-provisioning (#108's branch), since Transport B extends src/commands/gateway.rs which lives only there. If #108 merges to main mid-epic, the code is a superset — fine.
Children (stacked, foundation-first)
- Login-shell entry +
-c dispatch
- Caller identity via
tailscale whois (fail-closed)
owner column + gateway-boundary authz + admin allowlist
lilbox gateway install --user (+ RunSSH doctor check)
- SFTP/file-transfer hardening + Transport-B docs
Key correction from the spike: do not rely on $SSH_ORIGINAL_COMMAND — Tailscale SSH never sets it; read the command from the shell's own -c arg. And a restricted login shell does not block SFTP — close it at the server/ACL layer.
Overview
Make the
lilboxgateway work over Tailscale SSH with the caller's tailnet identity — no SSH keys, one local user, per-VM ownership in-app. End users install nothing: on the tailnet + ACL allows →ssh lilbox@host new --tailscale myboxjust works.De-risked in the spike #109 (findings there). Extends the gateway from #108 (key-based OpenSSH forced-command MVP).
Why an epic (not a sprint)
The children interlock on one runtime contract: the
-centry refactor gates identity resolution, identity gates ownership authz, both gate a meaningfulinstall, and hardening/docs describe the assembled whole. Any one alone onmainwould ship a half-wired gateway — so they stack on a shared integration branch and squash to trunk once.Base: the integration branch is cut from
paulg/gateway-ssh-provisioning(#108's branch), since Transport B extendssrc/commands/gateway.rswhich lives only there. If #108 merges to main mid-epic, the code is a superset — fine.Children (stacked, foundation-first)
-cdispatchtailscale whois(fail-closed)ownercolumn + gateway-boundary authz + admin allowlistlilbox gateway install --user(+RunSSHdoctor check)Key correction from the spike: do not rely on
$SSH_ORIGINAL_COMMAND— Tailscale SSH never sets it; read the command from the shell's own-carg. And a restricted login shell does not block SFTP — close it at the server/ACL layer.