Just wanted to cut to the chase about an unexpected issue I encountered while migrating to Docker Hardened Images in my GitHub Actions CI pipeline. I was building a multi-stage Dockerfile pulling multiple images from the dhi.io registry when I hit this error:
Error: buildx failed with: ERROR: failed to build: failed to solve: pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
This was despite having docker/login-action step configured for dhi.io, which always reported Login Succeeded!. What ended up working was directly configuring the docker/setup-buildx-action (BuildKit) step with dhi.io. This ensures BuildKit's daemon configuration includes the registry before any image pulls occur:
- name: Setup buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-config-inline: |
[registry."dhi.io"]
mirrors = ["dhi.io"]
I was able to reproduce this issue and created a repository where I demonstrate a failing pipeline without this configuration and a successful pipeline with this configuration. In the failing pipeline, I also demonstrated how the first stage will build just fine when targeted:
run: |
docker buildx build \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test \
--load \
--target=builder \
.
Whereas the second stage fails when targeted:
run: |
docker buildx build \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test \
--load \
--target=aot-cache-training-runner \
.
To be clear, this issue is probably not unique to the dhi.io registry; presumably it would happen with any private registry in multi-stage builds where BuildKit pulls multiple images in parallel. The root cause appears to be a race condition: docker builds can start pulling images before BuildKit's daemon has fully applied the registry configuration from docker/login-action.
Here are some takeaways for anyone encountering similar authentication issues:
- If docker/login-action succeeds but builds fail with "pull access denied," the issue is likely BuildKit daemon configuration, not credentials
- Use
buildkitd-config-inlineto explicitly configure registries before builds - This is especially important for multi-stage builds that pull from multiple private registries in parallel
Effectively it should look like this in GHA:
- name: Authenticate to Docker Hardened Image Registry
uses: docker/login-action@v3
with:
registry: someregistry.io
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Setup buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-config-inline: |
[registry."someregistry.io"]
mirrors = ["someregistry.io"]
As more teams adopt Docker Hardened Images and other enterprise level hardening solutions, understanding this BuildKit configuration quirk will save hours of debugging and get you closer to being well-architected. Feel free to add comments either here or on the sample repository, especially if it helped!
Top comments (0)