fix(azure): mint Azure Container Registry tokens through the registry's token service - #926
Merged
Conversation
added 3 commits
August 15, 2026 19:33
…'s token service An Azure Container Registry does not accept a Microsoft Entra token on its Docker Registry HTTP API v2 surface, and sockerless was presenting one. The auth provider asked Microsoft Entra for `https://<registry>/.default` — an audience Microsoft Entra does not issue, because Azure Container Registry has no per-registry audience — and put the result straight on /v2/ as a Bearer; the multi-architecture index writer did the same with an Azure Resource Manager token, beside a comment asserting the token exchange was only needed by tools wanting long-lived credentials. The documented flow now runs: a Microsoft Entra token for the container registry service audience `https://containerregistry.azure.net/.default`, exchanged at POST /oauth2/exchange for an Azure Container Registry refresh token, traded at POST /oauth2/token for an access token scoped to the access the operation needs, and that access token is the Bearer the data plane honours. Tokens are cached until the `exp` claim of the JWT the registry issued, and re-acquired when a registry answers 401. `core.AuthProvider.GetToken` carries the repository and Docker Registry HTTP API v2 actions; the AWS and Google Cloud providers document them as unused because their credentials are registry-wide. Around it, the registry endpoint coordinate became one thing: push, image listing, the multi-architecture index writer, the image-metadata fetch and the Artifact Registry tag probe all resolve it through `core.OCIRegistryBaseURL` and keep the Host header naming the registry through `core.SetOCIHost`. A failure to mint a registry credential surfaces instead of being retried anonymously in pull, push, build and listing. Covered end to end against the Microsoft Azure simulator in `backends/azure-common`: a registry provisioned through Azure Resource Manager, an image pushed over /v2/, a second tag added, the repository's tags read back with a metadata_read-scoped token, both tags removed, and an assertion that the credential presented is not the Microsoft Entra token it was exchanged for. Also in this change: - The simulators are pinned at the sockerless-cloud v0.9.2 release, verified complete: tests/go.mod at release commit 723736a8a233, twenty-three harness ARG SOCKERLESS_CLOUD_VERSION defaults at the same commit, and the git build context and AWS Lambda live-test checkout at the v0.9.2 tag. - The AWS SDK modules flagged by the dependency freshness check are upgraded across backends/{aws-common,ecs,lambda} and tests, verified by the Amazon ECS and AWS Lambda integration suites against the simulator. - The Amazon ECS and AWS Lambda harnesses reserve their own Amazon Route 53 resolver port, and every harness image build passes --load so its result reaches the container runtime's image store. BUGS 2938-2942 and 2947 closed; 2943-2946 filed.
… simulator The Azure Container Registry round trip built the simulator with a relative `-o` destination while running `go build` in the tests module directory, so the path resolved against that directory rather than the test's working directory and wrote the binary one level above the repository. The test then started the simulator from the path it meant all along, found nothing, and failed with "fork/exec ../../tests/.build/simulator-azure: no such file or directory". It passed locally only because `make install-simulators` had already put a real binary where the test looked, so the misplaced build output went unnoticed; CI's core job builds no simulators, so there it failed every time. Every other integration test in the repository already resolves the root with `filepath.Abs`, which is the convention this one missed. Verified by removing the prebuilt binary and reproducing the exact CI failure, then confirming the fix builds into the repository and the round trip passes from a clean tree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
backends/azure-common/acr_auth.goasked Microsoft Entra for a token scoped tohttps://<registry>.azurecr.io/.defaultand put that raw Entra token on theregistry's
/v2/endpoint as a Bearer. Real Azure Container Registry refusesthis. Microsoft documents it plainly:
The Go SDK agrees —
azcontainerregistryhardcodesdefaultAudience = "https://containerregistry.azure.net"for every cloud.The fix
A token service implementing the documented two-hop exchange: an Entra token for
the ACR service audience →
POST /oauth2/exchangefor a refresh token →POST /oauth2/tokenfor a scoped access token, which is what goes on/v2/. Tokensare cached per (service, scope) off the JWT
expwith a five-minute margin, anda 401 forces re-acquisition. There is no fallback to the old path — a fallback
would hide the bug.
Two details differ from what a reasonable reading would assume, both verified
rather than guessed:
expires_in.AcrAccessTokenin azure-rest-api-specs hasexactly one member,
access_token. Clients read the JWTexpclaim. A tokenwhose expiry cannot be read is therefore never cached, rather than being given
an invented TTL.
tags/listrequiresmetadata_read, notpull(Azure CLI's ownRepoAccessTokenPermission), and registry-level scopes carry only*.Access-token lifetime is undocumented — measured at 20 minutes against a live
registry — so nothing is hardcoded.
The scope has to reach the provider, so
core.AuthProvider.GetTokennow takesthe repository and actions. AWS and GCP document those arguments as unused,
their credentials being registry-wide.
AssembleMultiArchManifestwas using anARM-audience token behind a comment claiming the exchange was optional; it goes
through the same provider now and the false comment is gone.
core/registry.go'sGCP-host special case is generalised to "the provider already minted a Bearer",
which is what had been producing
Authorization: Basic Bearer ey…for ACRmetadata fetches.
Coverage
acr_registry_test.gostarts the pinned Azure simulator, provisions a registrythrough ARM, and performs a real push → tag → tag-list → remove over
/v2/,differing from the cloud only in coordinates. It asserts the presented
credential is not the Entra token it was exchanged for — the defect
expressed as a test. No harness previously set
SOCKERLESS_AZURE_ACR_ENDPOINT; this is the first.Honest limit: the pinned simulator (v0.9.2) still accepts any token on the ACR
data plane, so the test proves the correct flow runs but cannot fail via the
simulator's own enforcement. The enforcing simulator is on sockerless-cloud
mainand is not in a release yet.Pins
Bumped to v0.9.2, verified complete before pinning (30 assets, all three
multi-architecture indexes) — a tag can exist while its artifacts do not.
v0.10.0's release PR is still open. Every pin moved together:
tests/go.mod(pseudo-version off release commit
723736a8a233), 23 DockerfileSOCKERLESS_CLOUD_VERSIONdefaults,deploy/compose.build.yaml, and thelive-tests workflow ref.
Dependencies
30 drifted AWS SDK modules upgraded and verified against real wire shapes, not a
compile: Lambda and ECS integration suites both green.
Defects found while verifying, fixed here
SIM_DNS_PORT, so its Route 53 listener grabbed the default 5353 thatChrome's helper owns and the simulator panicked before serving.
--load, so buildxleft the image in the build cache and the workload could never pull it. This
presented as five Lambda tests timing out at 90s on a callback that never
came.
Filed, not fixed
Registry with no
docker login, and neither creates the target repositorythrough the control plane. Correct against the current pinned simulator,
wrong against the enforcing one, which is unreleased — so it can be neither
proven necessary nor regression-checked yet.
/v2/_catalog, so the round tripreads
tags/listdirectly instead ofcore.OCIListImages.NewARAuthProviderreadsconfig.EndpointURLwhileOverlayRegistryHostreadsSOCKERLESS_GCP_AR_ENDPOINT; they agree onlybecause the harnesses set both.