Give each job its own short-lived registry credential at start, read only for pulls and write only for the one repository it publishes to. Do not save a registry login on the runner host or in the image, because every job that runs there can then use it.
Why saved logins are the problem
GitLab's runner security guidance is explicit: Docker credentials kept in the runner host's own configuration files are available to every job the runner executes. On shared runners it says not to run docker login as the runner user, and not to keep Docker configuration files in the runner user's home directory.
That advice matters more when a platform starts many runners from one image. A login baked into the image or mounted into every pod is a credential shared by every job and every contributor who can trigger one.
Two separate needs
- Pulling the job image. The platform pulls the container image before the job starts. GitHub's job container syntax lets you set credentials for the image's registry, with a username and password map, the same values you would give
docker login. - Pulling and pushing inside the job. Steps that fetch base images or publish new ones need their own credentials.
Options compared
| Approach | Strength | Weakness |
|---|---|---|
| Login saved on runner host | Easy | Shared by all jobs, long lived |
| Credential baked into image | Easy | Anyone with the image has it |
| Long lived stored secret passed to job | Simple | One leak exposes the registry |
| Short lived token via OIDC or platform token | Expires, scoped per job | Needs registry support and setup |
| Pull-through mirror inside your network | No external credential in job | You maintain the mirror |
GitHub documents OIDC as a way to exchange short lived tokens with a cloud provider instead of duplicating long lived credentials as secrets. If your registry supports it, that is a good fit.
Steps
- Remove any registry login from runner hosts and images.
- Configure pulls of public base images through an internal mirror, so jobs need no credential for them.
- Issue a token per job, read only by default.
- Allow push rights only to the single repository and only from protected branches.
- Set a short expiry, ideally the length of the job.
- Log token issuance in the registry or identity system, so the trail survives the runner.
- Rotate any credential that ever sat on a shared runner image.
What to avoid in logs
A failed login can print the command with its arguments. Pass the password on standard input, not on the command line, and keep shell tracing off.
Quick check
Is a read only registry token safe to share across jobs?
It is lower risk than a write token, but it still lets anyone who steals it pull your images. Prefer per job tokens.
Do public base images need credentials?
Not for pulling. They can hit rate limits, so a mirror helps. See pull-through cache security trade-offs.
Sources
- GitLab Docs: Security for self-managed runners
- GitHub Docs: Running jobs in a container
- GitHub Docs: About security hardening with OpenID Connect
Last reviewed: 19 October 2026.