Skip to content

userEnvProbe (and --secrets-file) put container env values, incl. secrets, on the host docker exec argv #1317

Description

Summary

When userEnvProbe is enabled (the default, loginInteractiveShell), the CLI reads the container user's whole environment (cat /proc/self/environ) and passes every variable back to docker exec as a -e NAME=VALUE argument. This happens on every lifecycle hook (onCreateCommand, postCreateCommand, postStartCommand, …) and on every devcontainer exec.

So any secret that is already in the container's environment ends up in plaintext on the host process command line, where any local user can read it with ps. That includes secrets injected with runArgs: ["--env-file", …], containerEnv or an image ENV. Those variables are already part of the container's Config.Env, which docker exec inherits, so re-sending them on the command line gains nothing.

The --secrets-file feature has the same problem, even with "userEnvProbe": "none". Lifecycle commands merge the secrets into the exec env (const env = { ...(await remoteEnv), ...(await secrets) }), so each secret appears as -e NAME=VALUE on the host argv while the hook runs.

Where

On main (3e363f6):

  • src/spec-shutdown/dockerUtils.ts toDockerExecArgs (~L400-415): Object.keys(env).forEach(key => execArgs.push('-e', ${key}=${env[key]}))
  • src/spec-common/injectHeadless.ts probeUserEnv (~L777-789, cat /proc/self/environ) feeds remoteEnv. runLifecycleCommand (~L518) merges remoteEnv and secrets into that env.
  • In the published 0.89.0 bundle (dist/spec-node/devContainersSpecCLI.js) the same logic is the minified function KN: r&&Object.keys(r).forEach(a=>g.push("-e",${a}=${r[a]})).

Repro (dummy values only)

@devcontainers/cli 0.89.0, Docker Desktop on macOS, image alpine:3.

mkdir probe && cd probe
umask 077
printf 'ARGV_PROBE_VAR=DUMMY_NOT_SECRET\n' > dummy.env
printf '{"image":"alpine:3","overrideCommand":true,"runArgs":["--env-file","%s/dummy.env"]}\n' "$PWD" > .devcontainer.json
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . sleep 6 &
sleep 3; ps -axww -o command | grep '^docker exec'

Observed (other values elided):

docker exec -i -u root -e HOSTNAME=… -e SHLVL=… -e HOME=… -e PAGER=… -e LC_COLLATE=… -e PATH=… -e LANG=… -e CHARSET=… -e ARGV_PROBE_VAR=DUMMY_NOT_SECRET -w /workspaces/probe <id> sleep 6
Arm docker exec argv contains ARGV_PROBE_VAR=DUMMY_NOT_SECRET Var visible inside the container
default userEnvProbe yes (1 process) yes
"userEnvProbe": "none" no (0) yes (inherited from Config.Env)

--secrets-file arm: config {"image":"alpine:3","overrideCommand":true,"userEnvProbe":"none","postStartCommand":"sleep 8"} with --secrets-file secrets.json containing {"SECRETSFILE_PROBE_VAR":"DUMMY_NOT_SECRET_2"}. While the hook ran, the host showed docker exec -i -u root -e SECRETSFILE_PROBE_VAR=DUMMY_NOT_SECRET_2 -w /workspaces/probe-secretsfile <id> /bin/sh -c sleep 8.

We found this in a real setup where Doppler secrets were injected with --env-file. Every secret showed up in the host process table during devcontainer up and devcontainer exec.

Suggested fix

Any of these, in order of preference:

  1. Pass variables by name, not by value. Use docker exec -e NAME (no =), so docker reads the value from the CLI process's own environment, and spawn the docker child with { ...process.env, ...env }. Values never appear on argv. This also covers --secrets-file and remoteEnv.
  2. Or use docker exec --env-file <tmpfile>, written mode 0600 and removed after the exec starts.
  3. At minimum, don't re-send probed variables whose value already equals the container's Config.Env value. docker exec inherits those, so dropping them changes nothing. Only the delta the login shell adds would remain (e.g. PATH), which is rarely sensitive.

Workaround for users: set "userEnvProbe": "none" and run commands that need the login environment through bash -lc. This does not help with --secrets-file.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions