Authenticating GitLab Runner with Private Docker Registries

The integration of GitLab Runner with private container registries represents a critical junction in modern DevOps lifecycles, particularly when managing secure, enterprise-grade CI/CD pipelines. When a GitLab Runner utilizes the Docker executor, it must possess the requisite authority to pull base images and sidecar services from remote, non-public repositories. Failure to establish this authentication layer results in immediate job failure, often manifesting as "access forbidden" errors, even when the underlying host machine appears to have valid credentials. This complexity arises because the GitLab Runner operates within its own execution context, requiring specific configuration to bridge the gap between the runner's environment and the private registry's security protocols. Mastering this process requires a deep understanding of the DOCKER_AUTH_CONFIG variable, credential helpers, and the specific environmental requirements of the GitLab Runner architecture.

Architectural Requirements for Private Image Access

To successfully facilitate the pulling of images from a private registry, the GitLab Runner must be explicitly authenticated. This authentication is not a passive process; it requires the runner to possess either statically defined credentials or access to specialized binary helpers that can negotiate tokens with a registry provider.

Prerequisites for Registry Connectivity

Before attempting to configure authentication, certain technical prerequisites must be satisfied to ensure the runner can communicate with the target registry.

  • Authenticating the GitLab Runner to the specific registry endpoint.
  • Defining the image name using the full registry URI in the .gitlab-ci.yml file.
  • Ensuring the registry name and the image path are correctly formatted to allow the runner to search the correct namespace.

The Docker Executor and Image Definition

When using the Docker executor, the runner relies on a configuration that dictates which images are used for the primary job container and which are used for auxiliary services. If an image is not explicitly defined within the .gitlab-ci.yml file, the GitLab Runner defaults to the image specified in the config.toml file on the runner host.

For instance, a config.toml might be configured as follows:

toml [runners.docker] image = "ruby:3.3" [[runners.docker.services]] name = "mysql:latest" alias = "db" [[runners.docker.services]] name = "redis:latest" alias = "cache"

In this configuration, the runner uses ruby:3.3 as the primary environment. However, for private environments, this default must be overridden or updated to point to the private registry. The syntax for defining a private image follows a specific pattern:

image: my.registry.tld:5000/namespace/image:tag

In this scenario, the GitLab Runner interprets my.registry.tld:5000 as the target registry and proceeds to search that specific endpoint for the namespace/image:tag resource.

Implementation Strategies for DOCKERAUTHCONFIG

The primary mechanism for providing credentials to a GitLab Runner is the DOCKER_AUTH_CONFIG CI/CD variable. This variable holds a JSON structure that the Docker daemon uses to authenticate against various registries. There are two primary methods of implementation depending on the scope of the required access: per-job or per-runner.

Per-Job Configuration via CI/CD Variables

For scenarios where only specific jobs require access to a private registry, the DOCKER_AUTH_CONFIG variable can be defined directly within the GitLab project settings. This approach offers high granularity and isolation, ensuring that credentials are only exposed to the jobs that strictly require them.

To generate the required authentication string for a registry, users must create a base64-encoded version of their credentials. The process involves concatenating the username and password with a colon and then encoding the result.

$ echo -n "my_username:my_password" | base64

The resulting base64 string is then placed within the auths object of the JSON configuration. An example structure for a project-level variable would be:

json { "auths": { "registry.example.com:5000": { "auth": "T0Z0bm9uZ3Vlc191c2VybmFtZTpwYXNzd29yZA==" } } }

Per-Runner Configuration for Self-Managed Environments

In self-managed environments, it is often more efficient to configure the runner globally so that all jobs executed by that runner can access the private registry without needing individual variable definitions. This is accomplished by adding the DOCKER_AUTH_CONFIG as an environment variable within the runner's own configuration on the host machine.

Alternatively, for self-managed runners, the JSON configuration can be placed directly into the Docker configuration file located at the runner's home directory:

${GITLAB_RUNNER_HOME}/.docker/config.json

When the GitLab Runner starts, it reads this configuration file and utilizes the specified authentication details for the required repositories.

The Impact of Credential Stores and Registry Conflicts

A critical technical nuance involves the use of the credsStore property. If a configuration includes "credsStore": "osxkeychain", the Docker daemon will attempt to use the host's keychain to access registries. However, a significant conflict arises when a user attempts to mix public and private images.

If the configuration is set to use a specific credential store for all registries, the Docker daemon may attempt to use those same credentials for every registry request, including public ones like Docker Hub. This can result in failures when pulling public images because the Docker daemon is applying private registry credentials to the public Docker Hub registry.

Advanced Authentication with AWS ECR and Credential Helpers

For organizations utilizing Amazon Elastic Container Registry (ECR), static base64 authentication is often insufficient due to the ephemeral nature of ECR tokens. In these cases, the use of credential helpers is mandatory.

Deploying Credential Helpers

To use a credential helper such as docker-credential-ecr-login, the binary must be present in the GitLab Runner's $PATH. The runner must have the ability to execute this binary to retrieve fresh authorization tokens from AWS.

The configuration process for ECR requires the following steps:

  1. Ensure docker-credential-ecr-login is available in the GitLab Runner $PATH.
  2. Ensure the GitLab Runner Manager has the appropriate AWS credentials configured. The Manager acquires these credentials and passes them to the runners.
  3. Configure the DOCKER_AUTH_CONFIG to utilize the helper for the specific ECR registry.

ECR Configuration Workflow

When targeting an ECR registry, such as <aws_account_id>.dkr.ecr.<region>.amazonaws.com/private/image:latest, the runner needs to know which region to query. The region must be specified so the helper can retrieve the correct token.

The DOCKER_AUTH_CONFIG can be expanded to include multiple registries by adding them to the credHelpers hash within the JSON structure. This allows a single runner to manage access to multiple different ECR registries across different regions or even different cloud providers.

To generate a temporary password for ECR-based workflows, the following command can be utilized within a containerized environment to fetch the login password:

$ docker run --rm \ -e AWS_ACCESS_KEY_ID=<AWS_ACCESS_KEY_ID> \ -e AWS_SECRET_ACCESS_KEY=<AWS_SECRET_ACCESS_KEY> \ amazon/aws-cli ecr get-login-password \ --region <AWS_REGION>

Comprehensive ECR Job Example

The following is an exhaustive example of a .gitlab-ci.yml job that utilizes a private ECR image as the primary environment while simultaneously running standard public services like Postgres and Redis.

yaml test:api:dev: stage: test image: <AWS_ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/<NAMESPACE>:<TAG> services: - postgres:latest - redis:latest variables: POSTGRES_DB: data_api POSTGRES_USER: runner POSTGRES_PASSWORD: runner DATABASE_URL: postgres://runner:runner@postgres:5432/data_api script: - cd api - export DEBUG=1 - export ENVIRONMENT=dev - export CELERY_BROKER_URL=redis://redis - export CELERY_RESULT_BACKEND=redis://redis - python -m pytest -p no:warnings . - flake8 . - black --exclude="migrations|env" --check . - isort --skip=migrations --skip=env --check-only . - export DEBUG=0 - export ENVIRONMENT=prod - python manage.py check --deploy --fail-level=WARNING

In this example, the runner pulls the complex, private image from ECR, but it still successfully pulls the public postgres:latest and redis:latest images because the authentication is specifically scoped to the ECR registry through the credential helper or the auths mapping.

Networking and Security Considerations

Successfully pulling an image is only half of the operational requirement; the runner must also be able to network the containers correctly and ensure the integrity of the pulled assets.

Network Configuration for CI/CD Jobs

When running multiple containers (the main job image and various services), network connectivity is essential. There are two primary ways to handle this:

  • User-defined Docker bridge networks: It is highly recommended to configure the runner to create a new network for each individual job. This isolates the job's traffic and ensures that container environment variables are not inadvertently shared across different containers in a way that could lead to configuration leakage.
  • Container links: This is a legacy Docker feature. While still functional, it is considered outdated compared to modern user-defined bridge networks.

Image Integrity and Checksums

In high-security environments, simply pulling an image by tag is insufficient due to the risk of tag mutability (where a tag is moved to a different image digest). To maintain a rigorous security posture, users should utilize image checksums. By including the image checksum in the job definition within the .gitlab-ci.yml file, the GitLab Runner can verify the integrity of the image during the pull process, ensuring that the code being executed is exactly what was intended.

Troubleshooting Common Authentication Failures

Even with a correct DOCKER_AUTH_CONFIG, users frequently encounter issues where the runner claims access is forbidden.

The Host vs. Runner Discrepancy

A common point of confusion occurs when a user verifies that the host machine running the GitLab Runner can successfully execute docker pull for a private image. While the host might be authorized via a local ~/.docker/config.json, the GitLab Runner process may not be using that specific configuration. The Runner operates in its own context, and if the DOCKER_AUTH_CONFIG variable is missing or incorrectly formatted, the pull will fail regardless of the host's status.

Unprivileged User Limitations

If the --user flag is used to run child processes as an unprivileged user, the runner will use the home directory of the main runner process user. This has significant implications for credential access. Because credential stores and helpers require binaries to be added to the $PATH and require specific filesystem access, these features are unavailable on instance runners or any environment where the user does not have direct access to the environment where the runner is installed. This restriction necessitates that for managed or restricted environments, the use of statically defined credentials via DOCKER_AUTH_CONFIG is often the only viable path.

Analysis of Registry Integration Complexity

The process of connecting a GitLab Runner to a private registry is a multi-layered technical challenge that spans identity management, filesystem permissions, and network orchestration. The distinction between static authentication (using base64 encoded strings in DOCKER_AUTH_CONFIG) and dynamic authentication (using credential helpers like docker-credential-ecr-login) is the most critical decision a DevOps engineer must make. Static authentication is simpler to implement for standard registries but fails to scale in cloud-native environments like AWS where tokens expire rapidly. Conversely, credential helpers provide the necessary automation for cloud environments but introduce strict requirements regarding the $PATH and the runner's execution permissions.

The common pitfall of "credential leakage" across registries—where a single credsStore setting inadvertently attempts to apply private credentials to public repositories—highlights the need for precise, scoped configuration. To build a robust CI/CD pipeline, one must move beyond simple authentication and consider the holistic environment: ensuring network isolation through job-specific bridge networks, verifying image integrity via checksums, and accounting for the permission limitations of unprivileged runner users. Ultimately, the success of private registry integration depends on a configuration that is as granular as the security requirements of the organization.

Sources

  1. GitLab Documentation: Using Docker images
  2. GitLab Forum: Runner cannot pull images from private registry
  3. M. Herman: GitLab CI Private Docker Registry
  4. Mike Street: Deploying a Docker image to a remote private registry
  5. GitLab Documentation: Docker Executor

Related Posts