Skip to main content
RunBook Academy

Docker & ContainersXIII Β· RegistriesRegistry auth

Registry authentication β€” basic, token, OIDC

Intermediate⏱ ~22 mindocker

What you'll learn

  • Describe the token exchange behind a docker pull and what each token is scoped to
  • Locate every copy of a registry credential on a host and explain why base64 is not protection
  • Configure a credential helper so the plaintext credential never reaches the filesystem
  • Rotate a registry credential in an order that cannot break a running deploy

Prerequisites

Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12

Not yet marked complete on this device.

Registries support three authentication schemes:

  • Basic auth β€” username and password, sent over HTTPS.
  • Bearer token β€” a short-lived, scope-limited token issued by an auth service, passed as Authorization: Bearer ....
  • OIDC β€” delegated authentication, where the registry trusts an external identity provider rather than holding a password itself.

In practice they are not alternatives. Basic credentials are usually what you exchange for a bearer token, and OIDC changes what you present at that exchange. Understanding the sequence is what lets you diagnose a 401 correctly.

Basic auth, and where the credential goes

docker login registry.example.com
# Username: deployer
# Password:

The fix is a credential helper

A credential helper is a small external program the Docker CLI calls instead of writing the credential itself. The plaintext never touches config.json.

Configuration change~/.docker/config.json β€” credential store
{
"credsStore": "pass"
}

Per-registry helpers are also supported, which matters when one registry authenticates through a cloud IAM helper and another through a local keyring:

Configuration change~/.docker/config.json β€” per-registry helpers
{
"credsStore": "pass",
"credHelpers": {
  "registry.example.com": "secretservice"
}
}

On Linux the two common helpers are docker-credential-pass, which stores secrets in a GPG-backed pass store, and docker-credential-secretservice, which uses the D-Bus Secret Service (GNOME Keyring and equivalents). Both are separate binaries you install onto PATH; neither ships with the Docker CLI.

Read-only / Safeverify no plaintext credential remains
jq '.auths | map_values(has("auth"))' ~/.docker/config.json
{
"registry.example.com": false
}

Illustrative output

That is a verification that can fail, which is the point. β€œI configured a credential helper” is a claim; an empty auth field is evidence.

Never pass a password on the command line

docker login -u deployer -p "$DEPLOYER_PASSWORD" registry.example.com

This is the pattern every CI example uses and it is wrong in two ways: the value appears in the process list for as long as the command runs, and in the shell history of anyone who runs it interactively. Docker warns about it at runtime.

Configuration changethe warning you should never see in a pipeline log
$ docker login -u deployer -p REDACTED registry.example.com
WARNING! Using --password via the CLI is insecure. Use --password-stdin.

Illustrative output

Use standard input instead. The documentation’s justification is exact: β€œUsing STDIN prevents the password from ending up in the shell’s history, or log-files.”

Configuration changelogin from a secret without exposing it
printf '%s' "$DEPLOYER_TOKEN" \
| docker login registry.example.com --username deployer --password-stdin
Login Succeeded

Illustrative output

Bearer tokens

Bearer tokens are the mechanism doing most of the work, and they are invisible because docker login handles them.

The client presents its basic credential to an auth service, which returns a token scoped to a specific action on a specific repository β€” repository:library/nginx:pull, for example. That token, not the password, is what accompanies the manifest and blob requests. It is short-lived; the exact lifetime is set by the registry operator, and clients simply request a new one when the old one is refused.

Two operational consequences follow from the scoping:

  • A leaked token is bounded. It grants the actions named in its scope, on the repositories named in its scope, until it expires. A leaked password is not bounded at all β€” it mints new tokens.
  • A 401 on a push with a working pull is normal. The token was issued with pull scope because that is what the client asked for. The fix is a credential with push permission, not a network investigation.

OIDC and workload identity

OIDC moves the trust decision out of the registry. The registry stops holding a password and instead validates a token issued by an identity provider it trusts.

This is worth doing for three reasons that have nothing to do with cryptography:

  • Single sign-on for humans, so a departure revokes registry access as a side effect of revoking the account.
  • Workload identity for machines β€” a CI job or a Kubernetes service account presents an ephemeral, automatically rotated token rather than a long-lived password somebody pasted into a secret store years ago.
  • Auditing. The IdP logs the authentication with the real identity attached, rather than the registry logging twelve pipelines all presenting svc-ci.

The third one is usually the argument that lands, because β€œwho pushed this image” is a question every incident eventually asks.

Service accounts, not people

For anything automated, the credential must belong to a robot:

  • Scoped. Read-only for deploy hosts; push only for the CI job that publishes. A deploy host that can push is a deploy host that can poison the registry after one compromise.
  • Per-consumer. One credential per pipeline, not one shared across the estate. Shared credentials cannot be rotated without a synchronised outage and cannot be attributed in an audit log.
  • Named for its purpose. svc-ci-publish tells the next operator what breaks if they disable it. deployer2 does not.

Personal credentials in automation fail in a specific and predictable way: someone leaves, the account is disabled on their last day, and a pipeline nobody associated with that person stops working at the next run.

Rotation without an outage

  1. Issue the new credential alongside the old one. Both valid.
  2. Verify the new one works against the registry directly, before distributing it: a docker login plus a pull of a known digest from a scratch host.
  3. Distribute it to every consumer β€” secret manager, CI variables, host config, Kubernetes pull secrets, the pull-through cache.
  4. Prove no consumer is still using the old one. This is the step people skip, and it is the only one that makes the next step safe.
  5. Disable the old credential β€” do not delete it yet.
  6. Wait one full cycle of your slowest consumer (a nightly job, a weekly DR test) before deleting.

Step 4 needs evidence, and where you get it depends on the registry. Harbor, Quay, GitLab and the cloud registries all record the authenticating identity in an access log; a Distribution registry behind a reverse proxy records it in the proxy’s access log. Filter for the old identity and confirm the last entry predates your distribution step.

Read-only / Safelast use of the outgoing credential
LOGFILE=/var/log/nginx/registry-access.log
OLD_USER=deployer

grep -F "$OLD_USER" "$LOGFILE" | tail -5 || echo 'no recent use'
no recent use

Illustrative output

Knowledge check

Knowledge check Β· 5 questions

  1. Q1. With no credential store configured, how is a registry password protected in `~/.docker/config.json`?

  2. Q2. You run `docker login registry.example.com` successfully, then `sudo docker pull registry.example.com/api:1.4.0` fails with 401. Why?

  3. Q3. Which practices reduce the blast radius of a registry credential? Select all that apply.

  4. Q4. The correct first step when rotating a registry credential is to revoke the old one, so the exposure window closes immediately.

  5. Q5. The bearer token used for a `docker pull` is scoped to a specific action on a specific repository, so a leaked token grants less than a leaked password.

Passing score: 75%. Answers are checked in this browser.