fix: support non-interactive SSH on SSH Instances (VS Code Remote-SSH, scp/rsync, SFTP)#29
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes SSH-enabled Instance behavior so non-interactive SSH sessions (VS Code Remote-SSH bootstrap, ssh host <cmd>, scp/rsync, SFTP) execute correctly through the sshd sidecar’s ForceCommand chroot/nsenter confinement, and enables TCP/stream-local forwarding needed for VS Code port forwarding. It also bumps the operator’s default SSH sidecar image tag and adds an e2e guard + manual verification runbook.
Changes:
- Update
chroot.shto dispatch based on$SSH_ORIGINAL_COMMAND(interactive banner + login shell vs exec-channel command vs SFTP subsystem), while preserving capability-drop confinement. - Update the ssh-server image to enable forwarding and provide an SFTP subsystem backed by a statically built
github.com/pkg/sftpserver staged into themaincontainer filesystem. - Bump the default
instance-ssh-server-imagetogpustack/ssh-server:v1.3.0and add CASE 21 + a manual VS Code/sshfs verification guide.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
pkg/worker/settings/value.go |
Bumps default SSH sidecar image tag to v1.3.0. |
pack/ssh-server/rootfs/chroot.sh |
Adds exec-channel/SFTP routing using $SSH_ORIGINAL_COMMAND through a single confined enter() path; keeps interactive banner behavior. |
pack/ssh-server/Dockerfile |
Enables forwarding, pins SFTP subsystem path, and builds/copies a static SFTP server binary into the sidecar. |
docs/settings.md |
Updates documented default instance-ssh-server-image tag. |
.claude/skills/gpustack-operator-e2e/SKILL.md |
Documents new CASE 21 coverage and when it should run. |
.claude/skills/gpustack-operator-e2e/references/manual-ssh-verification.md |
Adds manual verification steps for VS Code Remote-SSH and sshfs. |
.claude/skills/gpustack-operator-e2e/cases/case-21.sh |
Adds an e2e case validating exec-channel behavior, SFTP round-trip into main, and TCP forwarding. |
- add case-21 asserting `ssh host '<cmd>'` runs in main with no banner - assert the exec path is capability-stripped and host mknod denied - keep interactive login (banner + shell) as a regression guard - round-trip a loopback tcp port-forward through the instance - register case 21 in the e2e skill case table and prose Task 0.1 of ssh-instance-noninteractive-ssh. Signed-off-by: thxCode <thxcode0824@gmail.com>
- dispatch chroot.sh on $SSH_ORIGINAL_COMMAND instead of always launching a login shell - empty request keeps the interactive banner + login shell, byte-for-byte - a command request runs `$SHELL -c "<cmd>"` in the target with no banner - factor the nsenter+chroot+cap-drop chain into a shared enter() so every path stays confined - move the tty clear into the interactive branch so it never corrupts a command stream Task 1.1 of ssh-instance-noninteractive-ssh. Signed-off-by: thxCode <thxcode0824@gmail.com>
- replace Alpine's active `AllowTcpForwarding no` with `yes` so port-forwarding works - add `AllowStreamLocalForwarding yes` for unix-socket forwarding - pin `Subsystem sftp /usr/lib/ssh/sftp-server`, replacing the shipped internal-sftp - drop the shipped active lines first so sshd reads exactly ours (no duplicate Subsystem) Task 1.2 of ssh-instance-noninteractive-ssh. Signed-off-by: thxCode <thxcode0824@gmail.com>
f574d9c to
b580e21
Compare
b580e21 to
2a5d1ac
Compare
- add a cross-compiled build stage for a CGO-free github.com/pkg/sftp server - serve SFTP over stdin/stdout so sshd can invoke it as the sftp subsystem - pin pkg/sftp v1.13.7 and strip the binary - COPY it into the image at /usr/local/bin/gpustack-sftp-server for staging into main Task 2.1 of ssh-instance-noninteractive-ssh. Signed-off-by: thxCode <thxcode0824@gmail.com>
- add an sftp branch to chroot.sh matching the pinned sftp-server path and internal-sftp - stage the bundled static server into the target's /tmp before the cap-drop - run it inside the target's rootfs so sftp/sshfs/scp operate on main's files - keep the same confinement and suppress the banner on this path Task 2.2 of ssh-instance-noninteractive-ssh. Signed-off-by: thxCode <thxcode0824@gmail.com>
- point instance-ssh-server-image at gpustack/ssh-server:v1.3.0 - sync the default in docs/settings.md Task 3.2 of ssh-instance-noninteractive-ssh. Signed-off-by: thxCode <thxcode0824@gmail.com>
- add an sftp put/get round-trip check to case-21 - assert the uploaded file lands in main's filesystem, not the sidecar - renumber the port-forward step and refresh the case header + SKILL.md prose Task 4.1 of ssh-instance-noninteractive-ssh. Signed-off-by: thxCode <thxcode0824@gmail.com>
- document a VS Code Remote-SSH connect + integrated terminal in main - document an sshfs mount round-trip through the bundled sftp-server - link the runbook from the e2e skill references Task 4.2 of ssh-instance-noninteractive-ssh. Signed-off-by: thxCode <thxcode0824@gmail.com>
2a5d1ac to
effdd69
Compare
- match the exact pinned sftp-server path so an exec command ending in /sftp-server is not silently rerouted to the sftp branch - stage the sftp-server atomically (temp inode then rename) so a concurrent session cannot hit ETXTBSY or a partial-write window - require EPERM (rc==1) in the case-21 mknod check, not any non-zero rc Follows the end-of-build review of ssh-instance-noninteractive-ssh. Signed-off-by: thxCode <thxcode0824@gmail.com>
effdd69 to
9636a5e
Compare
Comment on lines
+359
to
365
| REQUEST="${SSH_ORIGINAL_COMMAND:-}" | ||
|
|
||
| prepare_environment | ||
|
|
||
| nsenter_bin="$(command -v nsenter)" | ||
| chroot_bin="$(command -v chroot)" | ||
| setpriv_bin="$(command -v setpriv)" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SSH-enabled Instances render a two-container Pod (
main= user workload,sshd= Alpine sidecar) whose sshd routes every session throughForceCommand /chroot.sh, whichnsenters +chroots intomain, drops all capabilities, and launched an interactive login shell. The script never inspected$SSH_ORIGINAL_COMMAND: it unconditionally printed a banner and ran$SHELL -l, so any non-interactive request — VS Code Remote-SSH's server bootstrap,ssh host <cmd>, scp, rsync, the SFTP subsystem — had its command silently discarded and its stream corrupted by the banner. The remote server never started; VS Code then failed forwarding to the absent server with "TCP port forwarding may be disabled, or the remote server may have crashed" — the symptom in gpustack/gpustack#5801.This fixes the sidecar image (
pack/ssh-server) sochroot.shmirrors a stock SSH server, and enables forwarding insshd_config. The operator only bumps the defaultinstance-ssh-server-imagetag.Root cause (two independent causes)
chroot.shignored$SSH_ORIGINAL_COMMAND, so the non-interactive command never ran and the banner corrupted the stream.AllowTcpForwarding no, so even a running server could not forward.What changed
chroot.shdispatches on$SSH_ORIGINAL_COMMANDthrough oneenter()chokepoint that preserves the capability-drop confinement (--bounding-set=-all --inh-caps=-all): empty → interactive banner + login shell (byte-unchanged); the pinned sftp-server path /internal-sftp→ SFTP subsystem; else →$SHELL -c <cmd>(no banner).CGO_ENABLED=0)github.com/pkg/sftpbinary staged intomain(a musl sidecar server cannot exec in main's glibc rootfs;internal-sftpwould serve the sidecar's FS). Staged atomically (temp inode + rename) so concurrent sessions do not race on the running executable; matched by the exact pinned Subsystem path.sshd_configenablesAllowTcpForwarding/AllowStreamLocalForwardingand pinsSubsystem sftp.instance-ssh-server-image→gpustack/ssh-server:v1.3.0.gpustack-operator-e2eCASE 21) + a manual VS Code / sshfs runbook.Verification (real cluster)
ssh-server:v1.2.0: CASE 21 fails — command discarded, banner leaked, forwarding refused.ssh-server:v1.3.0: CASE 21 passes 9/9 (exec no-banner, interactive banner preserved, sftp put/get intomain, loopback TCP forward, capability-strip +mknoddenied). GPU-slice-over-SSH regressions (CASE 13/15) stay green./sftp-serverruns instead of being mis-routed.Release note
The release must publish
gpustack/ssh-server:v1.3.0(built from this branch'spack/ssh-server) alongside the operator so the default resolves. Already-running Instances pick up the new sidecar image on Pod recreation (no forced restart).Fixes gpustack/gpustack#5801.