skip to content

After a successful `docker login`, where does the Docker CLI keep the credentials it will send on later pushes and pulls, and what are the security implications of the default behaviour?

level: middleimportance: should knowfreq 40%

answer

  1. ~/.docker/config.json → auths → base64(user:pass)
  2. Base64 = encoding, not encryption
  3. Hub key is https://index.docker.io/v1/
  4. credsStore / credHelpers replace it
  5. DOCKER_CONFIG per job; --password-stdin

basics

~20 s

By default it writes an entry to ~/.docker/config.json under auths, keyed by registry host, holding base64(username:password). That is encoding, not encryption, so anyone who reads the file has the credential. docker logout <host> removes the entry.

solid answer

~50 s

`docker login registry.example.com` stores, in `$HOME/.docker/config.json`: ```json {"auths":{"registry.example.com":{"auth":"Y2ktYm90OnMzY3JldA=="}}} ``` The `auth` field is base64 of `username:password` — reversible with one command. Implications: - Any process running as that user, any backup, and any `COPY . .` in a build that includes the home directory recovers the secret. - It is per user and per machine, so a working laptop tells you nothing about a CI runner. - The file is keyed by registry host; being logged in to one registry grants nothing on another. - `DOCKER_CONFIG` relocates the directory, which is how you give a CI job an isolated config instead of polluting a shared home. The fix is to keep no plaintext at all: configure `credsStore`/`credHelpers` so a `docker-credential-*` binary supplies credentials from the OS keychain or mints short-lived tokens from a cloud identity. On shared build hosts I treat a plain `auths` entry as a finding, and I always pass secrets via `--password-stdin` so they never reach shell history or the process list.

code

bash · 19 lines
bash
# What is actually stored
cat ~/.docker/config.json
# {"auths":{"registry.example.com":{"auth":"Y2ktYm90OnMzY3JldA=="}}}

# Trivially reversible
echo 'Y2ktYm90OnMzY3JldA==' | base64 -d
# ci-bot:s3cret

# Never put the secret on the command line
docker login registry.example.com -u ci-bot --password-stdin < /run/secrets/registry_token

# Give a CI job its own throwaway config directory
export DOCKER_CONFIG=$(mktemp -d)
docker login registry.example.com -u ci-bot --password-stdin < /run/secrets/registry_token
docker push registry.example.com/team/app:1.2
rm -rf "$DOCKER_CONFIG"

# Remove a stored credential
docker logout registry.example.com

go deeper

for a junior

Know the credentials land in ~/.docker/config.json as base64 and that base64 is not encryption.

for a middle

Explain the per-host keying, the exposure paths (backups, build contexts, other processes), and that docker logout only removes the local entry.

for a senior

Drive to elimination of plaintext: credential helpers, ambient identity, per-job DOCKER_CONFIG, --password-stdin, and treating a plain auths entry on a shared host as a finding.

for a principal

Position it inside secret management: long-lived shared registry passwords should not exist, credentials should be machine-identity-derived and short-lived, and revocation should be a policy change.

## The default store The Docker CLI keeps its configuration in `$DOCKER_CONFIG` or, by default, `$HOME/.docker/`. A successful login adds an entry to `config.json`: ```json { "auths": { "registry.example.com": { "auth": "Y2ktYm90OnMzY3JldA==" }, "https://index.docker.io/v1/": { "auth": "..." } } } ``` Two details are worth knowing. First, the key is the **registry host** — Docker Hub is stored under the legacy key `https://index.docker.io/v1/`, which is why grepping for `docker.io` sometimes misses it. Second, `auth` is `base64(username + ":" + password)`. Base64 is a transport encoding with no key and no secrecy; `base64 -d` reverses it instantly. On subsequent operations the CLI reads this entry and uses it to authenticate the *token request* to the registry's authorization service (HTTP Basic over TLS), then passes the resulting bearer token to the daemon for the actual pull or push. ## Why the default is a problem - **Readable by anything running as that user.** File permissions protect it from other users, not from any process or dependency executing in your session — a compromised build script, a malicious package's install hook, or a debugging tool that uploads your home directory. - **It travels.** Home directories end up in backups, in VM snapshots, in container images when a Dockerfile does `COPY . .` from a context that includes it, and occasionally in support bundles. - **It is long-lived.** Unlike the short bearer tokens used per request, this credential does not expire on its own; a leak is exploitable until someone rotates it. - **It is often over-privileged.** People log in with a personal account or a shared robot that can push to many repositories, when the machine only ever pulls one. ## Better options, in order **1. Credential helpers.** Set `credsStore` (all registries) or `credHelpers` (per registry host) so the CLI invokes `docker-credential-<name>`: - OS-backed stores: `osxkeychain`, `wincred`, `pass`, `secretservice` — the same credential, protected by the platform's secret store instead of a dotfile. - Identity-derived helpers for cloud registries — no stored registry password at all; a fresh short-lived token is minted per request from the machine's ambient identity. With a helper configured, `auths` holds no secret and `docker login` routes to the helper's `store` verb. **2. Isolate the config per job.** `DOCKER_CONFIG=$(mktemp -d)` gives a CI job its own directory that is deleted at the end, instead of writing into a shared home that later jobs inherit. Pair it with a job-scoped token that expires anyway. **3. Never pass the secret as an argument.** `docker login -u user -p hunter2` puts the password into shell history and, briefly, into the process table where any local user can read it; the CLI prints a warning saying so. Use `--password-stdin` and pipe from a secret manager or a helper command. **4. Log out when done.** `docker logout <host>` removes the entry (and calls the helper's `erase`). In shared or long-lived environments this matters; in ephemeral ones the whole filesystem goes away, which is better still. ## Related fields in the same file `config.json` also stores non-secret settings — the credential helper mapping, experimental CLI flags, default platform, proxy settings that get injected as build args, and `HttpHeaders`. Reviewing it is a quick audit: which registries does this host hold credentials for, and are they plaintext? ## What auditors and interviewers listen for The crisp statement is: *base64 is not encryption; the default config is a plaintext credential at rest, scoped per user and per machine.* Follow it with the remedy (helpers, ambient identity, ephemeral config, `--password-stdin`) and the operational nuance that credentials are per registry host, so "I'm logged in" is always a question of *to what, as whom, on which machine*.

  • A Dockerfile does `COPY . .` and the build context is a home directory. What is the concrete risk?
    If `~/.docker/config.json` sits in the context, the base64 registry credential is baked into an image layer and travels wherever that image goes, including to anyone who can pull it. Layer content is inspectable, so deleting the file in a later instruction does not remove it from history. The mitigations are a `.dockerignore`, a tight build context, and not storing plaintext credentials in the first place.
  • How does the picture change once a credential helper is configured?
    `config.json` keeps only the `credsStore` or `credHelpers` mapping and usually an empty `auths` entry for the host; the secret lives in the OS keychain, or for identity-derived helpers is never persisted at all. The CLI shells out to the helper on every registry operation, so credentials are fetched fresh rather than read from disk. Leaking the config file then discloses configuration, not a usable secret.

saying these in an interview costs you the question

  • Describing the `auth` value as encrypted or hashed
  • Assuming the Docker daemon, not the CLI, holds the credentials
  • Believing one login covers every registry
  • Using `-p` on the command line and dismissing the CLI's warning
  • Thinking `docker logout` invalidates the credential server-side rather than just deleting the local entry

context