Skip to content

🤖 Docker credential sharing is not portable across non-root users and host-specific Git helpers #3819

Description

@alecsg77

Summary

Docker workspace credential sharing currently assumes that the host global Git configuration can be copied into a container unchanged. That assumption fails in two related ways:

  1. DockerRuntime.setupCredentials() copies the host config to <container>:/root/.gitconfig, while Docker workspace images can run as a non-root default user.
  2. Even when the file is written to the effective container user’s home directory, a host Git config can reference credential helpers, includes, commands, sockets, files, keyrings, or network configuration that are not available inside the container.

The first is a concrete bug. The second is a broader design question about the supported contract for Git and SSH credential forwarding in Docker and Dev Container workspaces.

Current behavior

  • src/node/runtime/DockerRuntime.ts calls setupCredentials() both after fresh provisioning and when postCreateSetup() reuses an already-valid forked container.
  • setupCredentials() currently uses docker cp to copy the host config to /root/.gitconfig, then runs gh auth setup-git when GH_TOKEN is available.
  • Fresh provisioning detects the container uid/gid/home before credential setup, but the reuse/fork branch invokes credential setup before containerHome has been cached.
  • DevcontainerRuntime.setupCredentials() already uses a more user-correct transport: it reads the host config and writes it through cat > "$HOME/.gitconfig" as the configured container user. It also forwards a broader set of Coder-related environment/mount inputs.

Relevant prior work to re-evaluate:

Example: Coder workspace credentials

In some Coder-backed environments, the host Git config contains a credential helper that depends on Coder-specific executables, agent state, environment variables, or mounts. Copying that config into a generic Docker workspace does not make those dependencies available. The helper may fail, block a supported fallback, or point Git at paths that do not exist in the container.

This is not unique to Coder. Similar examples include OS keychain helpers, Git Credential Manager, libsecret, password-manager helpers, enterprise helpers, custom shell helpers, and helpers backed by local files or sockets.

Why copying a global Git config is not a portable credential transport

Potentially non-portable configuration includes:

  • credential.helper, including multiple helpers and URL-scoped helpers;
  • shell or absolute-path helpers;
  • helper state files, daemons, sockets, and browser/OAuth flows;
  • [include] and [includeIf] files, including conditional configuration activated by the container’s Git directory, branch, or remote URL;
  • core.sshCommand, IdentityFile, ProxyCommand, SSH certificate/known-host paths, and other SSH-wrapper configuration;
  • custom CA bundles, proxies, client certificates, VPN/DNS assumptions, or URL rewrites;
  • signing configuration (gpg.program, SSH signing) and Git external tools/filters/hooks that point to host-only executables.

Copying the file can therefore produce a configuration that is syntactically valid but operationally broken. Blind filtering is also non-trivial: Git config supports quoting, repeated values, URL subsections, and includes, and removing all credential.* settings could discard valid provider-specific username or policy configuration.

Lifecycle impact

The issue should consider at least these paths:

Lifecycle Risk
Fresh provisioning Non-root home/ownership and helper portability
Reuse/fork Credential setup can run before user-home metadata is cached
Runtime recreated for an existing workspace In-memory home metadata is absent
Restart/upgrade of an image Helper binaries, daemons, and sockets may no longer exist
Private submodules during provisioning .mux/init runs too late to repair credentials needed for bundle sync, checkout, or submodule materialization

Existing supported mechanisms vs. opaque host helpers

Mux already has mechanisms with an explicit transport contract:

  • GH_TOKEN followed by gh auth setup-git;
  • SSH agent forwarding;
  • for Dev Containers, selected GIT_ASKPASS/CODER_* environment forwarding and an optional /.coder-agent mount.

These should be considered separately from copying arbitrary host configuration. A copied Git config says which helper Git should invoke; it does not provide the helper’s executable, dependencies, session, or secrets.

.mux/init as an escape hatch

A repository can provide an executable .mux/init script. In Docker and Dev Container runtimes it runs in the workspace directory as the effective container user and receives workspace/project secrets via environment variables.

This can help teams with known, container-native setups, for example:

  • run gh auth setup-git against an intentionally supplied GH_TOKEN;
  • configure a credential helper that is installed in the image;
  • remove or replace a known incompatible helper;
  • configure an enterprise provider from project secrets;
  • verify or adjust a forwarded SSH agent.

However, it is not a complete solution:

  • it is non-interactive (GIT_TERMINAL_PROMPT=0, no TTY);
  • it cannot reconstruct an opaque host helper or host keyring/session;
  • it runs after Docker provisioning, checkout, and submodule materialization, so it cannot repair credentials needed before those stages;
  • it is repository-controlled and only appropriate for trusted projects.

Questions for design discussion

  1. What is the supported credential-portability contract for Docker versus Dev Container workspaces?
  2. Should Mux distinguish between portable Git preferences, explicitly supported credential transports, and host-specific helper configuration that is intentionally not imported?
  3. Should Docker adopt the DevcontainerRuntime transport pattern (readHostGitconfig() + this.exec('cat > "$HOME/.gitconfig"')) as the minimal fix for the non-root/reuse bug, while treating helper portability as a separate problem?
  4. Should the default behavior be full config copy, selective/sanitized copy, only explicitly supported transports, or opt-in integrations for known helpers/providers?
  5. If .mux/init is the intended extensibility point, do we need an earlier lifecycle hook for credentials required during clone/submodule setup?
  6. Would a redacted credential diagnostic be useful (effective user/home, configured helper origins, active includes, gh/GH_TOKEN/SSH-agent/Coder-agent availability, and lifecycle phase of failure)?

Suggested acceptance criteria for an initial narrow fix

  • Docker writes any imported Git config as the actual default container user, using $HOME, not hardcoded /root.
  • The reuse/fork path works when containerHome has not yet been cached.
  • Raw config bytes are streamed via stdin rather than interpolated into a shell command.
  • Existing GH_TOKEN and SSH-agent behavior remains intact.
  • Tests cover root and non-root users, fresh provisioning and reused/forked containers, including the uncached-home reuse case.
  • The implementation and documentation explicitly state that this alone does not make host-specific credential helpers portable.

Generated with mux • Model: openai:gpt-5.6-terra • Thinking: high • Cost: $0.15

Metadata

Metadata

Assignees

No one assigned

    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