Credential helpers instead of plaintext passwords
A plain docker login stores credentials by default in plaintext in ~/.docker/config.json, readable by any process with file access. Credential helpers solve this by delegating passwords to a secure keystore, both locally and in CI pipelines.
Table of Contents
- 1. How docker login stores credentials by default
- 2. How credential helpers solve the problem
- 3. Setting up cloud-specific credential helpers
- 4. Credential helpers on Linux without a desktop environment
- 5. Passing secrets safely to docker login in CI environments
- 6. Credential helpers in Compose and Kubernetes environments
- 7. The least-privilege principle for registry credentials
- 8. Token rotation and audit capability
- 9. Best practices and a comparison of methods
- 10. Summary
- 11. FAQ
1. How docker login stores credentials by default
After a docker login against a private registry, Docker stores the credentials in the file ~/.docker/config.json, in a section called auths, organized by registry hostname. Without further configuration, the username and password are by default merely base64-encoded there, which at first glance looks like encryption but is in fact trivially reversible and offers no cryptographic protection whatsoever. Any process and any user with read access to this file can decode the credentials within seconds.
This becomes especially critical on shared development machines, CI runners with multiple parallel jobs, or backup systems that back up a user's home directory, since config.json can then potentially end up in backups, snapshots, or be viewed by other processes on the same system. On a personal laptop the risk may be manageable, but in a production CI/CD environment with many contributors and automated processes, base64 encoding as the sole protection mechanism for registry credentials is clearly inadequate.
2. How credential helpers solve the problem
A credential helper is a standalone executable program that instructs Docker to stop storing credentials directly in config.json and instead delegate them to a secure, OS-native or cloud-specific store, such as the macOS Keychain, the Windows Credential Manager, the Linux Secret Service via libsecret, or cloud-specific mechanisms like IAM roles on AWS. Docker communicates with the helper program via a simple, standardized stdin/stdout protocol that implements commands such as store, get, and erase.
A credential helper is configured via the credsStore or credHelpers key in ~/.docker/config.json. credsStore sets a single global helper for all registries, while credHelpers allows a fine-grained mapping of a helper per registry hostname, which is useful, for example, when an AWS ECR registry authenticated via IAM credentials and a Docker Hub login via the local OS keystore need to coexist.
# The credential helper binary must be on the PATH, e.g. docker-credential-osxkeychain
which docker-credential-osxkeychain
# Configure ~/.docker/config.json manually or via docker login
cat ~/.docker/config.json
# {
# "credsStore": "osxkeychain"
# }
3. Setting up cloud-specific credential helpers
Dedicated credential helpers exist for the major cloud registries that can additionally issue temporary, automatically rotating access tokens instead of a permanent password. For AWS ECR that is docker-credential-ecr-login, which uses the existing AWS configuration, such as an IAM role or a profile from ~/.aws/credentials, to automatically request a short-lived authentication token on every docker pull or docker push, without a static password ever ending up in any configuration file.
Similar helpers exist for Google Artifact Registry with docker-credential-gcr and for Azure Container Registry with docker-credential-acr, each tightly integrated with the cloud provider's own identity and access management. The big advantage over a classic docker login with a long-lived password is that the actual credentials never sit permanently on disk and, if a machine is compromised, automatically expire on the next token refresh instead of requiring manual rotation.
# Install and configure the ECR credential helper
brew install docker-credential-ecr-login # or via your distribution's package manager
cat ~/.docker/config.json
# {
# "credHelpers": {
# "123456789012.dkr.ecr.eu-central-1.amazonaws.com": "ecr-login"
# }
# }
# No docker login needed, IAM role or profile authenticates automatically
docker pull 123456789012.dkr.ecr.eu-central-1.amazonaws.com/mironsoft/app:latest
4. Credential helpers on Linux without a desktop environment
On Linux servers and CI runners without a graphical desktop environment, the convenient libsecret-based helper is often unavailable, since it relies on a running secret service daemon such as GNOME Keyring. An alternative is docker-credential-pass, built on the established Unix tool pass, which stores credentials GPG-encrypted in a simple filesystem tree and therefore also works on headless servers without a graphical interface.
Setup requires an initialized GPG key and an initialized pass store, after which docker-credential-pass behaves like any other credential helper and is activated via credsStore in config.json. It is important that the GPG key itself in turn be kept safe, for example in a hardware token or a dedicated secrets management system, since it ultimately forms the root protection for all stored registry credentials.
# Setting up docker-credential-pass on Linux
gpg --gen-key
pass init "docker-credential-key-id"
echo '{"credsStore": "pass"}' > ~/.docker/config.json
docker login registry.mironsoft.internal
5. Passing secrets safely to docker login in CI environments
In CI pipelines, an interactive docker login with a password prompt is not practical, which is why credentials are usually passed via --password-stdin, avoiding the password appearing as a plaintext argument in the process listing or in shell history. A common mistake is using the deprecated --password flag with a direct value, since this can potentially end up in job logs, the runner's bash history file, or be visible to other processes on the same system via ps aux.
CI-native secret mechanisms such as GitHub Actions Secrets or GitLab CI/CD variables ensure the value itself appears masked in logs and does not need to be stored in plaintext in the workflow or pipeline file. Combined with --password-stdin, the password is thus never visible as a command-line argument but passed to docker login exclusively via a secure pipe.
# Secure login in CI via stdin instead of a command-line argument
echo "$REGISTRY_PASSWORD" | docker login ghcr.io -u "$REGISTRY_USER" --password-stdin
# Never do this (password ends up in process listing and history):
# docker login ghcr.io -u user --password "$REGISTRY_PASSWORD"
6. Credential helpers in Compose and Kubernetes environments
docker compose uses the same ~/.docker/config.json as the Docker CLI when pulling images, so a once-configured credential helper automatically applies to compose pull and compose build with private base images too, without separate configuration. In Kubernetes environments, however, no credential helper in the strict sense is used; instead, imagePullSecrets store credentials as a Kubernetes secret of type kubernetes.io/dockerconfigjson in the cluster and are attached to a pod or service account.
In managed Kubernetes offerings like EKS, GKE, or AKS, however, node-side credential providers, such as the ECR credential provider for EKS, frequently take on tasks similar to a local credential helper: they automatically supply short-lived tokens for the respective cloud registry, so no static imagePullSecret needs to be maintained and credentials rotate automatically at regular intervals, analogous to the local ecr-login helper.
7. The least-privilege principle for registry credentials
Regardless of the specific storage mechanism, registry credentials are subject to the same least-privilege principle as any other form of credentials: a CI job that only needs to pull images should never be equipped with a token that also has push or even delete rights. Many registries allow the creation of fine-grained access tokens or service accounts scoped to individual repositories and operations, instead of a generic admin access shared by all automated processes.
Especially in shared CI environments with many projects, it is advisable to issue narrowly scoped credentials per project or even per pipeline stage, so that a compromised token in the worst case only affects a single repository and does not endanger the entire registry infrastructure. This segmentation increases the initial configuration effort but significantly reduces the potential damage in the event of a security incident.
8. Token rotation and audit capability
Static, long-lived passwords, as arise from a classic docker login without a credential helper, must be rotated manually, which in practice is often neglected once an access works. Cloud credential helpers such as docker-credential-ecr-login structurally avoid this problem by issuing only short-lived tokens in the first place, typically valid for a few hours, so a stolen token automatically becomes useless once it expires, without any human intervention needed.
In addition, many registries offer detailed audit logs that record every pull and push together with the access token used, which, combined with fine-grained credentials issued per project, allows precise traceability of which service or pipeline accessed which image and when. With a single, shared admin access for all systems, this traceability is largely lost, since individual actions can no longer be clearly attributed to a source.
9. Best practices and a comparison of methods
As a practical ground rule: on developer laptops, an OS-native credential helper such as osxkeychain, wincred, or pass should always be enabled, never plain base64 storage in config.json. In CI pipelines, cloud-native credential providers with short-lived tokens should be preferred over static passwords wherever available, and where that is not possible, at least --password-stdin combined with masked CI secrets should be used.
The table below compares the presented mechanisms in terms of security, place of use, and maintenance effort, to make the right choice easier for your own environment.
| Mechanism | Storage | Place of use | Rotation |
|---|---|---|---|
| docker login without helper | Base64 in config.json, no protection | Not recommended | Manual |
| OS credential helper (osxkeychain, wincred, pass) | Encrypted OS keystore | Developer laptops | Manual |
| Cloud credential helper (ecr-login, gcr, acr) | No permanent password, token on demand | CI pipelines, cloud environments | Automatic, short-lived |
| --password-stdin with CI secrets | Masked CI secret, no plaintext in logs | CI pipelines without a cloud helper | Manual via CI secret rotation |
Mironsoft
Container infrastructure, CI pipelines and deployment automation
Docker setups that hold up across the team and in production?
We review existing Dockerfiles and Compose stacks for security gaps, bloated images and fragile build pipelines, then build a container infrastructure that builds fast, runs securely and stays understandable across the team.
Dockerfile Review
Systematically optimizing multi-stage builds, layer caching and image size.
Security Audit
Hardening container isolation, secrets handling and image scanning against real attack surfaces.
CI/CD Integration
Building build pipelines, registries and deployment strategies for reproducible releases.
10. Summary
Registry Authentication: Key Takeaways
Core problem
Without a helper, docker login stores credentials only base64-encoded, not encrypted.
Credential helpers
Delegate credentials to an OS keystore or cloud IAM instead of storing them in config.json.
Cloud advantage
ecr-login, gcr, and acr issue short-lived tokens instead of static passwords.
CI practice
--password-stdin with masked CI secrets, never --password in plaintext.