Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions gearbox-agent/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ WORKDIR /app
COPY --from=go-builder /build/gearbox-agent .
COPY --from=go-builder --chown=65532:65532 /var/lib/gearbox-agent /var/lib/gearbox-agent

# busybox (statically linked, musl) supplies the `nsenter` applet for the
# console gear's host-exec mode (#142). When the agent runs with
# pid:host + privileged on a Docker host, sessions need to setns into
# host PID 1's namespaces — but the host's own nsenter binary can't be
# exec'd from the container because its ELF interpreter
# (/lib64/ld-linux-x86-64.so.2 + libc) isn't visible in the distroless
# mount namespace. busybox is statically linked, ~1MB, and dispatches
# applets by argv[0], so dropping it in as /usr/bin/nsenter is enough.
# Operators not running host-exec mode pay only the 1MB image cost.
#
# Pinned by index digest (not just tag) so the build is reproducible
# across busybox tag rebuilds. The buildx multi-arch resolver picks
# the right platform manifest from this index automatically. busybox
# 1.37.0's nsenter applet supports -t/--target, -m/--mount,
# -u/--uts, -i/--ipc, -n/--net, -p/--pid (which is what SpawnNsenter
# emits); verified against the upstream applet list before pinning.
COPY --from=busybox:1.37.0-musl@sha256:19b646668802469d968a05342a601e78da4322a414a7c09b1c9ee25165042138 /bin/busybox /usr/bin/nsenter

# Expose API port
EXPOSE 8405

Expand Down
48 changes: 28 additions & 20 deletions gearbox-agent/internal/api/console/pty/nsenter_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,37 +111,45 @@ func nsenterUsable() bool {
return true
}

// hostNsenterCandidates is the search list for the host's nsenter
// binary as seen from inside the container. With pid:host the kernel
// exposes the host's root filesystem at /proc/1/root (a magic symlink
// resolved by the host PID 1's mount namespace), so absolute paths
// under /proc/1/root/... reach the host's util-linux install.
// nsenterCandidates is the search list for an nsenter binary the
// agent can exec. Container-local paths come first because they're
// guaranteed to have a working ELF interpreter in the agent's mount
// namespace; the official agent image bundles a statically-linked
// busybox at /usr/bin/nsenter for exactly this reason.
//
// Order: most-common location first; /usr/bin covers Debian/Ubuntu/
// modern RHEL; /bin covers older trees; /usr/sbin and /sbin cover
// distros that consider nsenter a privileged util.
var hostNsenterCandidates = []string{
// The /proc/1/root candidates remain as a last-ditch fallback for
// non-distroless agent flavors that happen to share enough libc
// layout with the host to make exec succeed — but in practice, with
// the official distroless image, the kernel resolves the host's
// nsenter binary fine for stat() yet fails the subsequent execve()
// because the binary's PT_INTERP (e.g. /lib64/ld-linux-x86-64.so.2)
// isn't visible in the container's mount namespace.
var nsenterCandidates = []string{
// Container-local (bundled by Dockerfile, or operator-installed):
"/usr/bin/nsenter",
"/bin/nsenter",
"/usr/sbin/nsenter",
"/sbin/nsenter",
// Host fallback via /proc/1/root — only loads when libc paths
// happen to line up. Kept for completeness; never relied on.
// Mirrors the container-local list (incl. sbin) so non-distroless
// agent flavors that match host layout get parity coverage.
"/proc/1/root/usr/bin/nsenter",
"/proc/1/root/bin/nsenter",
Comment thread
sarg3nt marked this conversation as resolved.
"/proc/1/root/usr/sbin/nsenter",
"/proc/1/root/sbin/nsenter",
}
Comment thread
sarg3nt marked this conversation as resolved.

// resolveHostNsenter returns the first reachable nsenter binary path
// from hostNsenterCandidates, or "" if none exist. Cached results are
// not appropriate — operator upgrades on the host could move the
// binary; the call is cheap (a few stat()s).
// resolveHostNsenter returns the first reachable nsenter binary path,
// or "" if none exist. Not cached — cheap stat()s, and operator
// changes (image upgrade, host package install) could affect the
// answer between calls.
func resolveHostNsenter() string {
for _, p := range hostNsenterCandidates {
for _, p := range nsenterCandidates {
if st, err := os.Stat(p); err == nil && !st.IsDir() {
return p
}
}
// Last-ditch: maybe the container itself ships nsenter (some
// non-distroless agent flavors might). exec.LookPath uses $PATH.
if _, err := os.Stat("/usr/bin/nsenter"); err == nil {
return "/usr/bin/nsenter"
}
return ""
}

Expand All @@ -167,7 +175,7 @@ func SpawnNsenter(ctx context.Context, command []string, runAs string, cols, row
}
nsenterBin := resolveHostNsenter()
if nsenterBin == "" {
return nil, errors.New("nsenter: binary not found (looked under /proc/1/root and /usr/bin)")
return nil, fmt.Errorf("nsenter: binary not found (searched %v)", nsenterCandidates)
}
argv := append([]string{
nsenterBin,
Expand Down
Loading