mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 04:06:58 +00:00
The route was registered GET-only, so containerd and Docker, which try the OAuth2 POST endpoint first whenever they hold a secret, ate a 405 and retried on the GET form. Every authenticated pull paid two auth round trips, and in the production logs the POST share of token traffic grew from 0.5% to 38% over six weeks as more clients pulled from k8s with basic-auth imagePullSecrets. Serve both specs on the same path. After credentials and scope are extracted the two paths are identical, so this is an extraction branch plus a form-shaped error writer. Only grant_type=password is supported and no refresh token is issued: the registry JWT's lifetime is pinned to the AppView<->hold service-auth, so a refresh token would be a fourth long-lived credential with its own storage and revocation. Clients handle its absence by continuing to use the credential they already hold. The refresh grant is refused with 401 rather than the 400 that RFC 6749 5.2 prescribes. containerd sends that grant only when it has no username, which is the same condition that disables its 405 fallback, so a 400 would hard-fail those clients. 401 is on its retry list and routes them to the GET form, where a device secret authenticates off the password alone. That shape previously had no working path at all. resolveService now takes the requested service as an argument, since it arrives in the query string on GET and in the form body on POST. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>