From 3b9bd002467daa3d4f96a77696caca0bb2b0a260 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Fri, 24 Jul 2026 10:39:19 -0500 Subject: [PATCH 1/3] feat(docker): add optional privilege elevation for Docker and Podman providers Rootful Docker/Podman daemons expose a socket accessible only to root and the docker group, so the local provider fails with a permission error for users outside that group. Adding an explicit, opt-in privilege-elevation mechanism lets those users run the provider without joining the group or running the whole process as root. Add a DOCKER_ELEVATION / PODMAN_ELEVATION option (none, pkexec, sudo, doas) that wraps every docker/podman invocation with the chosen helper. To avoid a prompt per command and to keep the interactive prompt out of the short per-command probe timeouts, authentication is performed once up front at driver construction, warming the OS credential cache for the rest of the operation. Client-only runtime detection stays unelevated. --- pkg/docker/elevate.go | 87 ++++++++++++++++++++++++++++++++++ pkg/docker/elevate_test.go | 79 ++++++++++++++++++++++++++++++ pkg/docker/helper.go | 10 +++- pkg/driver/docker/docker.go | 28 ++++++++--- pkg/options/resolve.go | 1 + pkg/provider/provider.go | 5 ++ providers/docker/provider.yaml | 5 ++ providers/podman/provider.yaml | 5 ++ 8 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 pkg/docker/elevate.go create mode 100644 pkg/docker/elevate_test.go diff --git a/pkg/docker/elevate.go b/pkg/docker/elevate.go new file mode 100644 index 000000000..082f8c637 --- /dev/null +++ b/pkg/docker/elevate.go @@ -0,0 +1,87 @@ +package docker + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + "strings" + "sync" + "time" + + "github.com/devsy-org/devsy/pkg/log" +) + +// elevationAuthTimeout is generous enough for interactive credential entry, +// unlike the short per-command probe timeouts. +const elevationAuthTimeout = 2 * time.Minute + +// Elevator runs docker commands through a privilege-elevation helper (pkexec, +// sudo, doas). It authenticates once, up front, so an operation's many commands +// share a single prompt via the warmed OS credential cache. +type Elevator struct { + prefix []string // elevation command and leading args; always non-empty + + once sync.Once + err error +} + +// ElevatorFromName maps a helper name to an Elevator; "" and "none" return +// (nil, nil), unknown names error. +func ElevatorFromName(name string) (*Elevator, error) { + switch strings.ToLower(strings.TrimSpace(name)) { + case "", "none": + return nil, nil + case "pkexec": + return &Elevator{prefix: []string{"pkexec"}}, nil + case "sudo": + return &Elevator{prefix: []string{"sudo"}}, nil + case "doas": + return &Elevator{prefix: []string{"doas"}}, nil + default: + return nil, fmt.Errorf("unknown privilege elevation %q (want pkexec, sudo, doas, or none)", name) + } +} + +func (e *Elevator) wrap(dockerCommand string, args []string) (string, []string) { + full := make([]string, 0, len(e.prefix)+len(args)) + full = append(full, e.prefix[1:]...) + full = append(full, dockerCommand) + full = append(full, args...) + return e.prefix[0], full +} + +// ensureAuthenticated warms the credential cache once. It elevates the +// client-only "--version" (no daemon needed) on its own timeout, so a short +// caller deadline cannot kill the prompt. +func (e *Elevator) ensureAuthenticated(dockerCommand string, env []string) error { + e.once.Do(func() { + ctx, cancel := context.WithTimeout(context.Background(), elevationAuthTimeout) + defer cancel() + + name, args := e.wrap(dockerCommand, []string{"--version"}) + cmd := exec.CommandContext(ctx, name, args...) + if env != nil { + cmd.Env = append(os.Environ(), env...) + } + cmd.Stdin = os.Stdin // attach terminal for the credential prompt + cmd.Stdout = io.Discard + cmd.Stderr = os.Stderr + + log.Debugf("authenticating privilege elevation via %s", e.prefix[0]) + if err := cmd.Run(); err != nil { + e.err = fmt.Errorf("privilege elevation via %s failed: %w", e.prefix[0], err) + } + }) + return e.err +} + +// EnsureElevated authenticates the configured elevator once; concurrent callers +// block on the single prompt. No-op and safe when no elevator is set. +func (r *DockerHelper) EnsureElevated() error { + if r.Elevator == nil { + return nil + } + return r.Elevator.ensureAuthenticated(r.DockerCommand, r.Environment) +} diff --git a/pkg/docker/elevate_test.go b/pkg/docker/elevate_test.go new file mode 100644 index 000000000..16179db6f --- /dev/null +++ b/pkg/docker/elevate_test.go @@ -0,0 +1,79 @@ +package docker + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestElevatorFromName(t *testing.T) { + tests := []struct { + name string + wantNil bool + wantPrefix []string + wantErr bool + }{ + {name: "", wantNil: true}, + {name: "none", wantNil: true}, + {name: " None ", wantNil: true}, + {name: "pkexec", wantPrefix: []string{"pkexec"}}, + {name: "SUDO", wantPrefix: []string{"sudo"}}, + {name: "doas", wantPrefix: []string{"doas"}}, + {name: "gksu", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e, err := ElevatorFromName(tt.name) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tt.wantNil { + assert.Nil(t, e) + return + } + require.NotNil(t, e) + assert.Equal(t, tt.wantPrefix, e.prefix) + }) + } +} + +func TestElevatorWrap(t *testing.T) { + e, err := ElevatorFromName("pkexec") + require.NoError(t, err) + + name, args := e.wrap("docker", []string{"ps", "-q"}) + assert.Equal(t, "pkexec", name) + assert.Equal(t, []string{"docker", "ps", "-q"}, args) + + // No docker args. + name, args = e.wrap("/usr/bin/docker", nil) + assert.Equal(t, "pkexec", name) + assert.Equal(t, []string{"/usr/bin/docker"}, args) +} + +func TestEnsureElevatedNoOpWithoutElevator(t *testing.T) { + r := &DockerHelper{DockerCommand: "docker"} + assert.NoError(t, r.EnsureElevated()) +} + +func TestBuildCmdWithoutElevator(t *testing.T) { + r := &DockerHelper{DockerCommand: "docker"} + cmd := r.buildCmd(t.Context(), "ps", "-q") + assert.Equal(t, []string{"docker", "ps", "-q"}, cmd.Args) +} + +func TestBuildCmdWithElevator(t *testing.T) { + e, err := ElevatorFromName("sudo") + require.NoError(t, err) + // Mark authentication as already done so buildCmd does not attempt an + // interactive prompt during the test. + e.once.Do(func() {}) + + r := &DockerHelper{DockerCommand: "docker", Elevator: e} + cmd := r.buildCmd(t.Context(), "ps", "-q") + assert.Equal(t, []string{"sudo", "docker", "ps", "-q"}, cmd.Args) +} diff --git a/pkg/docker/helper.go b/pkg/docker/helper.go index e2c574242..696076e0a 100644 --- a/pkg/docker/helper.go +++ b/pkg/docker/helper.go @@ -68,6 +68,9 @@ type DockerHelper struct { Environment []string Builder DockerBuilder Runtime ContainerRuntime + // Elevator, when set, runs docker commands through a privilege-elevation + // helper (pkexec/sudo/doas). Nil disables elevation. + Elevator *Elevator } func (r *DockerHelper) GPUSupportEnabled() (bool, error) { @@ -523,7 +526,12 @@ func failedBootSentinel(status string, graceElapsed bool) error { } func (r *DockerHelper) buildCmd(ctx context.Context, args ...string) *exec.Cmd { - cmd := exec.CommandContext(ctx, r.DockerCommand, args...) + name, cmdArgs := r.DockerCommand, args + if r.Elevator != nil { + _ = r.EnsureElevated() // defensive; normally already done in NewDockerDriver + name, cmdArgs = r.Elevator.wrap(r.DockerCommand, args) + } + cmd := exec.CommandContext(ctx, name, cmdArgs...) if r.Environment != nil { cmd.Env = append(os.Environ(), r.Environment...) } diff --git a/pkg/driver/docker/docker.go b/pkg/driver/docker/docker.go index e300b7043..1e5c5b007 100644 --- a/pkg/driver/docker/docker.go +++ b/pkg/driver/docker/docker.go @@ -59,15 +59,29 @@ func NewDockerDriver( rt = docker.DetectRuntime(dockerCommand) } + elevator, err := docker.ElevatorFromName(workspaceInfo.Agent.Docker.Elevation) + if err != nil { + return nil, fmt.Errorf("invalid elevation config: %w", err) + } + log.Debugf("using docker command: command=%s, runtime=%s", dockerCommand, rt.Name()) + dockerHelper := &docker.DockerHelper{ + DockerCommand: dockerCommand, + Environment: makeEnvironment(workspaceInfo.Agent.Docker.Env), + ContainerID: workspaceInfo.Workspace.Source.Container, + Builder: builder, + Runtime: rt, + Elevator: elevator, + } + + // Authenticate before any command runs, keeping the prompt out of the short + // per-command probe timeouts that would otherwise kill it. + if err := dockerHelper.EnsureElevated(); err != nil { + return nil, err + } + return &dockerDriver{ - Docker: &docker.DockerHelper{ - DockerCommand: dockerCommand, - Environment: makeEnvironment(workspaceInfo.Agent.Docker.Env), - ContainerID: workspaceInfo.Workspace.Source.Container, - Builder: builder, - Runtime: rt, - }, + Docker: dockerHelper, IDLabels: workspaceInfo.CLIOptions.IDLabels, UpdateRemoteUserUIDDefault: workspaceInfo.CLIOptions.UpdateRemoteUserUIDDefault, }, nil diff --git a/pkg/options/resolve.go b/pkg/options/resolve.go index 7aa58aa3c..4313f123f 100644 --- a/pkg/options/resolve.go +++ b/pkg/options/resolve.go @@ -282,6 +282,7 @@ func resolveAgentDockerConfig( options map[string]string, ) { agentConfig.Docker.Path = resolver.ResolveDefaultValue(agentConfig.Docker.Path, options) + agentConfig.Docker.Elevation = resolver.ResolveDefaultValue(agentConfig.Docker.Elevation, options) agentConfig.Docker.Builder = resolver.ResolveDefaultValue(agentConfig.Docker.Builder, options) agentConfig.Docker.Install = types.StrBool( resolver.ResolveDefaultValue(string(agentConfig.Docker.Install), options), diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 4e6783dbd..ad0896f1c 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -202,6 +202,11 @@ type ProviderDockerDriverConfig struct { // Runtime identifies the container runtime explicitly (docker, podman, nerdctl). // When empty, the runtime is auto-detected from the binary at Path. Runtime string `json:"runtime,omitempty"` + + // Elevation optionally runs docker commands through a privilege-elevation + // helper for rootful daemons whose socket is not accessible to the current + // user. One of "" / "none" (disabled), "pkexec", "sudo", or "doas". + Elevation string `json:"elevation,omitempty"` } type ProviderKubernetesDriverConfig struct { diff --git a/providers/docker/provider.yaml b/providers/docker/provider.yaml index b2056e0b1..fd8b70bcd 100644 --- a/providers/docker/provider.yaml +++ b/providers/docker/provider.yaml @@ -8,6 +8,7 @@ optionGroups: - options: - DOCKER_PATH - DOCKER_HOST + - DOCKER_ELEVATION - INACTIVITY_TIMEOUT - DOCKER_BUILDER name: "Advanced Options" @@ -17,6 +18,9 @@ options: DOCKER_PATH: description: The path where to find the docker binary. default: docker + DOCKER_ELEVATION: + description: "Optionally run docker commands through a privilege-elevation helper for rootful daemons whose socket the current user cannot access. One of: pkexec, sudo, doas, or none. Note: pkexec targets local desktop sessions with a running polkit agent; use sudo or doas on headless/SSH hosts." + default: none DOCKER_HOST: global: true description: The docker host to use. @@ -28,6 +32,7 @@ agent: local: true docker: path: ${DOCKER_PATH} + elevation: ${DOCKER_ELEVATION} builder: ${DOCKER_BUILDER} install: false env: diff --git a/providers/podman/provider.yaml b/providers/podman/provider.yaml index be4f1f185..0a2d45ded 100644 --- a/providers/podman/provider.yaml +++ b/providers/podman/provider.yaml @@ -8,6 +8,7 @@ optionGroups: - options: - PODMAN_PATH - PODMAN_HOST + - PODMAN_ELEVATION - INACTIVITY_TIMEOUT name: "Advanced Options" options: @@ -16,6 +17,9 @@ options: PODMAN_PATH: description: The path where to find the podman binary. default: podman + PODMAN_ELEVATION: + description: "Optionally run podman commands through a privilege-elevation helper for a rootful podman socket the current user cannot access. One of: pkexec, sudo, doas, or none. Leave as none for rootless podman: elevating would target the separate rootful podman instead. Note: pkexec targets local desktop sessions with a running polkit agent; use sudo or doas on headless/SSH hosts." + default: none PODMAN_HOST: global: true description: The podman host to use. @@ -24,6 +28,7 @@ agent: local: true docker: path: ${PODMAN_PATH} + elevation: ${PODMAN_ELEVATION} install: false runtime: podman env: From c0fa726fbe31d4b9a1bc038b0d20061e3a912bb1 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Fri, 24 Jul 2026 10:47:30 -0500 Subject: [PATCH 2/3] fix(docker): satisfy linters for elevation code Extract pkexec/sudo/doas string constants (goconst), wrap long lines (golines), and mark the trusted-config exec calls //nolint:gosec (G204). --- pkg/docker/elevate.go | 25 ++++++++++++++++++------- pkg/docker/elevate_test.go | 30 ++++++++++++++++-------------- pkg/docker/helper.go | 1 + pkg/options/resolve.go | 5 ++++- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/pkg/docker/elevate.go b/pkg/docker/elevate.go index 082f8c637..9a00bebfd 100644 --- a/pkg/docker/elevate.go +++ b/pkg/docker/elevate.go @@ -17,6 +17,13 @@ import ( // unlike the short per-command probe timeouts. const elevationAuthTimeout = 2 * time.Minute +// Supported privilege-elevation helpers. +const ( + elevationPkexec = "pkexec" + elevationSudo = "sudo" + elevationDoas = "doas" +) + // Elevator runs docker commands through a privilege-elevation helper (pkexec, // sudo, doas). It authenticates once, up front, so an operation's many commands // share a single prompt via the warmed OS credential cache. @@ -33,14 +40,17 @@ func ElevatorFromName(name string) (*Elevator, error) { switch strings.ToLower(strings.TrimSpace(name)) { case "", "none": return nil, nil - case "pkexec": - return &Elevator{prefix: []string{"pkexec"}}, nil - case "sudo": - return &Elevator{prefix: []string{"sudo"}}, nil - case "doas": - return &Elevator{prefix: []string{"doas"}}, nil + case elevationPkexec: + return &Elevator{prefix: []string{elevationPkexec}}, nil + case elevationSudo: + return &Elevator{prefix: []string{elevationSudo}}, nil + case elevationDoas: + return &Elevator{prefix: []string{elevationDoas}}, nil default: - return nil, fmt.Errorf("unknown privilege elevation %q (want pkexec, sudo, doas, or none)", name) + return nil, fmt.Errorf( + "unknown privilege elevation %q (want pkexec, sudo, doas, or none)", + name, + ) } } @@ -61,6 +71,7 @@ func (e *Elevator) ensureAuthenticated(dockerCommand string, env []string) error defer cancel() name, args := e.wrap(dockerCommand, []string{"--version"}) + //nolint:gosec // command and args come from trusted provider config cmd := exec.CommandContext(ctx, name, args...) if env != nil { cmd.Env = append(os.Environ(), env...) diff --git a/pkg/docker/elevate_test.go b/pkg/docker/elevate_test.go index 16179db6f..7eded1f8e 100644 --- a/pkg/docker/elevate_test.go +++ b/pkg/docker/elevate_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/require" ) +const dockerCmd = string(RuntimeDocker) + func TestElevatorFromName(t *testing.T) { tests := []struct { name string @@ -17,9 +19,9 @@ func TestElevatorFromName(t *testing.T) { {name: "", wantNil: true}, {name: "none", wantNil: true}, {name: " None ", wantNil: true}, - {name: "pkexec", wantPrefix: []string{"pkexec"}}, - {name: "SUDO", wantPrefix: []string{"sudo"}}, - {name: "doas", wantPrefix: []string{"doas"}}, + {name: elevationPkexec, wantPrefix: []string{elevationPkexec}}, + {name: "SUDO", wantPrefix: []string{elevationSudo}}, + {name: elevationDoas, wantPrefix: []string{elevationDoas}}, {name: "gksu", wantErr: true}, } @@ -42,38 +44,38 @@ func TestElevatorFromName(t *testing.T) { } func TestElevatorWrap(t *testing.T) { - e, err := ElevatorFromName("pkexec") + e, err := ElevatorFromName(elevationPkexec) require.NoError(t, err) - name, args := e.wrap("docker", []string{"ps", "-q"}) - assert.Equal(t, "pkexec", name) - assert.Equal(t, []string{"docker", "ps", "-q"}, args) + name, args := e.wrap(dockerCmd, []string{"ps", "-q"}) + assert.Equal(t, elevationPkexec, name) + assert.Equal(t, []string{dockerCmd, "ps", "-q"}, args) // No docker args. name, args = e.wrap("/usr/bin/docker", nil) - assert.Equal(t, "pkexec", name) + assert.Equal(t, elevationPkexec, name) assert.Equal(t, []string{"/usr/bin/docker"}, args) } func TestEnsureElevatedNoOpWithoutElevator(t *testing.T) { - r := &DockerHelper{DockerCommand: "docker"} + r := &DockerHelper{DockerCommand: dockerCmd} assert.NoError(t, r.EnsureElevated()) } func TestBuildCmdWithoutElevator(t *testing.T) { - r := &DockerHelper{DockerCommand: "docker"} + r := &DockerHelper{DockerCommand: dockerCmd} cmd := r.buildCmd(t.Context(), "ps", "-q") - assert.Equal(t, []string{"docker", "ps", "-q"}, cmd.Args) + assert.Equal(t, []string{dockerCmd, "ps", "-q"}, cmd.Args) } func TestBuildCmdWithElevator(t *testing.T) { - e, err := ElevatorFromName("sudo") + e, err := ElevatorFromName(elevationSudo) require.NoError(t, err) // Mark authentication as already done so buildCmd does not attempt an // interactive prompt during the test. e.once.Do(func() {}) - r := &DockerHelper{DockerCommand: "docker", Elevator: e} + r := &DockerHelper{DockerCommand: dockerCmd, Elevator: e} cmd := r.buildCmd(t.Context(), "ps", "-q") - assert.Equal(t, []string{"sudo", "docker", "ps", "-q"}, cmd.Args) + assert.Equal(t, []string{elevationSudo, dockerCmd, "ps", "-q"}, cmd.Args) } diff --git a/pkg/docker/helper.go b/pkg/docker/helper.go index 696076e0a..3bd5409f1 100644 --- a/pkg/docker/helper.go +++ b/pkg/docker/helper.go @@ -531,6 +531,7 @@ func (r *DockerHelper) buildCmd(ctx context.Context, args ...string) *exec.Cmd { _ = r.EnsureElevated() // defensive; normally already done in NewDockerDriver name, cmdArgs = r.Elevator.wrap(r.DockerCommand, args) } + //nolint:gosec // command and args come from trusted provider config cmd := exec.CommandContext(ctx, name, cmdArgs...) if r.Environment != nil { cmd.Env = append(os.Environ(), r.Environment...) diff --git a/pkg/options/resolve.go b/pkg/options/resolve.go index 4313f123f..d3d7a833d 100644 --- a/pkg/options/resolve.go +++ b/pkg/options/resolve.go @@ -282,7 +282,10 @@ func resolveAgentDockerConfig( options map[string]string, ) { agentConfig.Docker.Path = resolver.ResolveDefaultValue(agentConfig.Docker.Path, options) - agentConfig.Docker.Elevation = resolver.ResolveDefaultValue(agentConfig.Docker.Elevation, options) + agentConfig.Docker.Elevation = resolver.ResolveDefaultValue( + agentConfig.Docker.Elevation, + options, + ) agentConfig.Docker.Builder = resolver.ResolveDefaultValue(agentConfig.Docker.Builder, options) agentConfig.Docker.Install = types.StrBool( resolver.ResolveDefaultValue(string(agentConfig.Docker.Install), options), From b0a6d4cc92e6f9939b4113c2f4f47a4897318e7f Mon Sep 17 00:00:00 2001 From: Samuel K Date: Fri, 24 Jul 2026 13:53:43 -0500 Subject: [PATCH 3/3] fix(docker): forward provider env through elevation helper sudo/pkexec/doas reset the child environment, so provider-configured variables such as DOCKER_HOST (set via the docker/podman env block) were silently dropped when elevation was active. Forward them explicitly through env(1) so the elevated docker command receives the intended configuration. --- pkg/docker/elevate.go | 13 ++++++++++--- pkg/docker/elevate_test.go | 31 ++++++++++++++++++++++++++++--- pkg/docker/helper.go | 2 +- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/pkg/docker/elevate.go b/pkg/docker/elevate.go index 9a00bebfd..933e9b591 100644 --- a/pkg/docker/elevate.go +++ b/pkg/docker/elevate.go @@ -54,9 +54,16 @@ func ElevatorFromName(name string) (*Elevator, error) { } } -func (e *Elevator) wrap(dockerCommand string, args []string) (string, []string) { - full := make([]string, 0, len(e.prefix)+len(args)) +// wrap builds the elevated invocation of dockerCommand. env (KEY=VAL entries, +// e.g. DOCKER_HOST) is forwarded through env(1) because sudo/pkexec/doas reset +// the child environment and would otherwise drop provider configuration. +func (e *Elevator) wrap(dockerCommand string, env, args []string) (string, []string) { + full := make([]string, 0, len(e.prefix)+len(env)+len(args)+1) full = append(full, e.prefix[1:]...) + if len(env) > 0 { + full = append(full, "env") + full = append(full, env...) + } full = append(full, dockerCommand) full = append(full, args...) return e.prefix[0], full @@ -70,7 +77,7 @@ func (e *Elevator) ensureAuthenticated(dockerCommand string, env []string) error ctx, cancel := context.WithTimeout(context.Background(), elevationAuthTimeout) defer cancel() - name, args := e.wrap(dockerCommand, []string{"--version"}) + name, args := e.wrap(dockerCommand, env, []string{"--version"}) //nolint:gosec // command and args come from trusted provider config cmd := exec.CommandContext(ctx, name, args...) if env != nil { diff --git a/pkg/docker/elevate_test.go b/pkg/docker/elevate_test.go index 7eded1f8e..d8ab0a6fc 100644 --- a/pkg/docker/elevate_test.go +++ b/pkg/docker/elevate_test.go @@ -7,7 +7,10 @@ import ( "github.com/stretchr/testify/require" ) -const dockerCmd = string(RuntimeDocker) +const ( + dockerCmd = string(RuntimeDocker) + dockerHost = "DOCKER_HOST=tcp://host:2375" +) func TestElevatorFromName(t *testing.T) { tests := []struct { @@ -47,14 +50,19 @@ func TestElevatorWrap(t *testing.T) { e, err := ElevatorFromName(elevationPkexec) require.NoError(t, err) - name, args := e.wrap(dockerCmd, []string{"ps", "-q"}) + name, args := e.wrap(dockerCmd, nil, []string{"ps", "-q"}) assert.Equal(t, elevationPkexec, name) assert.Equal(t, []string{dockerCmd, "ps", "-q"}, args) // No docker args. - name, args = e.wrap("/usr/bin/docker", nil) + name, args = e.wrap("/usr/bin/docker", nil, nil) assert.Equal(t, elevationPkexec, name) assert.Equal(t, []string{"/usr/bin/docker"}, args) + + // Environment is forwarded through env(1). + name, args = e.wrap(dockerCmd, []string{dockerHost}, []string{"ps"}) + assert.Equal(t, elevationPkexec, name) + assert.Equal(t, []string{"env", dockerHost, dockerCmd, "ps"}, args) } func TestEnsureElevatedNoOpWithoutElevator(t *testing.T) { @@ -79,3 +87,20 @@ func TestBuildCmdWithElevator(t *testing.T) { cmd := r.buildCmd(t.Context(), "ps", "-q") assert.Equal(t, []string{elevationSudo, dockerCmd, "ps", "-q"}, cmd.Args) } + +func TestBuildCmdWithElevatorForwardsEnv(t *testing.T) { + e, err := ElevatorFromName(elevationSudo) + require.NoError(t, err) + e.once.Do(func() {}) + + r := &DockerHelper{ + DockerCommand: dockerCmd, + Environment: []string{dockerHost}, + Elevator: e, + } + cmd := r.buildCmd(t.Context(), "ps") + assert.Equal(t, + []string{elevationSudo, "env", dockerHost, dockerCmd, "ps"}, + cmd.Args, + ) +} diff --git a/pkg/docker/helper.go b/pkg/docker/helper.go index 3bd5409f1..c6a7a7988 100644 --- a/pkg/docker/helper.go +++ b/pkg/docker/helper.go @@ -529,7 +529,7 @@ func (r *DockerHelper) buildCmd(ctx context.Context, args ...string) *exec.Cmd { name, cmdArgs := r.DockerCommand, args if r.Elevator != nil { _ = r.EnsureElevated() // defensive; normally already done in NewDockerDriver - name, cmdArgs = r.Elevator.wrap(r.DockerCommand, args) + name, cmdArgs = r.Elevator.wrap(r.DockerCommand, r.Environment, args) } //nolint:gosec // command and args come from trusted provider config cmd := exec.CommandContext(ctx, name, cmdArgs...)