Describe the HTTP handshake a container client performs against a registry that uses token authentication: what the registry's 401 response and its `WWW-Authenticate: Bearer` header carry, and what the client does with them.
answer
- 401 → WWW-Authenticate: Bearer realm/service/scope
- GET realm?service=&scope= with Basic creds
- JWT with access claims, minutes-long TTL
- Retry with Authorization: Bearer
- push = scope pull,push
basics
~20 sThe client requests a resource, gets 401 with WWW-Authenticate: Bearer realm=..., service=..., scope=.... It calls that realm (the token service) with its credentials and the requested scope, receives a short-lived signed token listing allowed actions, then retries the original request with Authorization: Bearer <token>.
solid answer
~60 sRegistries using the token scheme are stateless: they hold no sessions, they verify signed tokens. 1. The client sends the request unauthenticated, e.g. `GET /v2/team/app/manifests/1.0`. 2. The registry answers **401** with `WWW-Authenticate: Bearer realm="https://auth.example.com/token", service="registry.example.com", scope="repository:team/app:pull"`. `realm` is the token service URL, `service` names the registry the token is for, `scope` states what is being asked for. 3. The client calls the realm URL with `service` and `scope` as query parameters, authenticating itself — typically HTTP Basic with the credentials from `docker login`, or whatever the credential helper produces. 4. The token service authenticates the identity, decides which of the requested actions to grant, and returns JSON with a **short-lived signed token** (a JWT) whose access claims list `repository:team/app` with actions like `["pull"]`. A denied action is simply absent — the service can return a token narrower than requested. 5. The client retries with `Authorization: Bearer <token>` and caches it until it expires. A push needs `pull,push` scope, so `docker push` triggers a second handshake with a wider scope than a pull.
code
bash · 14 lines# 1. Provoke the challenge
curl -sI https://registry.example.com/v2/team/app/manifests/1.0
# HTTP/1.1 401 Unauthorized
# Www-Authenticate: Bearer realm="https://auth.example.com/token",service="registry.example.com",scope="repository:team/app:pull"
# 2. Exchange credentials for a scoped token
TOKEN=$(curl -s -u "$USER:$SECRET" \
"https://auth.example.com/token?service=registry.example.com&scope=repository:team/app:pull" \
| jq -r .token)
# 3. Retry the original request with the bearer token
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.oci.image.index.v1+json" \
https://registry.example.com/v2/team/app/manifests/1.0go deeper
Know that a 401 tells the client where to fetch a token and that the request is then retried with a bearer token.
Name the three challenge parameters, describe the token exchange, and explain that scope is per repository plus actions.
Use the handshake as a diagnostic: separate 401-at-token-service from 403-at-registry, and know about redirects to blob storage and proxy interference.
Discuss the design: stateless registries, an external identity provider fronting many registries, short-lived unrevocable tokens, and the blast radius that implies.
## Why a token dance at all The Registry HTTP API v2 separates two roles: the **registry** stores and serves content, and an **authorization service** decides who may do what. Splitting them lets one identity provider front many registries, lets the registry stay stateless (it validates a signature instead of looking up a session), and lets a token be minted narrowly — for one repository and one set of actions — rather than granting blanket access. Basic authentication is also supported by some registries, but the token scheme is what Docker Hub, Harbor, GHCR, ECR and friends use in practice. ## Step by step **1. Unauthenticated attempt.** The client just issues the API call: `GET /v2/`, `GET /v2/team/app/manifests/1.0`, `HEAD /v2/team/app/blobs/sha256:...`, and so on. It does not pre-emptively attach credentials. **2. The challenge.** If the resource needs authorization, the registry replies `401 Unauthorized` with a challenge header: ``` WWW-Authenticate: Bearer realm="https://auth.example.com/token",service="registry.example.com",scope="repository:team/app:pull" ``` - `realm` — the URL of the token service to call. It is often a *different* host from the registry itself. - `service` — an identifier for the registry the token must be valid for; it goes into the token's audience so a token minted for one registry cannot be replayed at another. - `scope` — what the client is being asked to prove rights for, formatted `<type>:<name>:<actions>`, e.g. `repository:team/app:pull` or `repository:team/app:pull,push`. Cross-repository blob mounts produce two repository scopes in one request. Catalog listing uses `registry:catalog:*`. **3. Token request.** The client GETs the realm with the challenge parameters echoed back: `GET https://auth.example.com/token?service=registry.example.com&scope=repository:team/app:pull`. It authenticates *this* request with the credentials it holds for the registry host — usually HTTP Basic (`Authorization: Basic base64(user:secret)`), which is why the connection must be HTTPS. An anonymous client simply omits credentials and may still receive a token for public repositories. **4. Token issuance.** The service authenticates the identity, evaluates its policy, and returns JSON: ```json {"token":"eyJhbGciOi...","access_token":"eyJhbGciOi...","expires_in":300,"issued_at":"2026-01-01T00:00:00Z"} ``` The token is a JWT signed by the service. Its claims include the issuer, the audience (`service`), an expiry, and an `access` array describing the granted scopes and actions. Two consequences matter in interviews: - **The grant can be narrower than the request.** If you ask for `pull,push` but only have read rights, you get a token with `pull` only — the request does not fail at this step. The push then fails later with 403 when you try to upload. - **The registry never contacts the auth service.** It verifies the JWT signature against a trusted public key and reads the access claims. That is what makes the registry horizontally scalable and why tokens are deliberately short-lived (minutes). **5. Retry.** The client repeats the original request with `Authorization: Bearer <token>` and gets 200. It caches the token keyed by scope and reuses it until expiry, so a multi-layer pull performs the handshake once, not once per blob. When a pull touches a new repository — for example a manifest that references a foreign layer or a cross-repo mount — a fresh handshake occurs for that scope. ## Practical implications - **Push needs a wider scope than pull.** `docker push` requests `pull,push`; that is why an identity with read-only rights can log in successfully and still fail at the first blob upload. - **Blob downloads often redirect.** After authorization, a blob GET frequently returns `307` to object storage with a pre-signed URL. That URL carries its own signature, so the `Authorization` header must *not* be forwarded to it — a classic proxy misconfiguration that breaks pulls or, worse, leaks the bearer token to a third-party host. - **Debugging.** `curl -v https://registry/v2/` shows the challenge; you can replay each step by hand, which is the fastest way to prove whether a failure is authentication (bad credentials → 401 from the token service) or authorization (token issued but lacking the action → 403 from the registry). - **Corporate proxies** that strip `WWW-Authenticate` or rewrite `realm` break the flow entirely, producing puzzling anonymous-only behaviour. ## Where credentials come from The client needs *something* to authenticate the token request: an entry saved by `docker login`, or output from a credential helper that mints a fresh secret per call. Cloud registries prefer the latter because their underlying credentials are themselves short-lived.
- You request scope `repository:team/app:pull,push` but the token you get back only grants `pull`. What happens next?The token request itself succeeds — the auth service returns the subset of actions your identity is entitled to rather than an error. The pull-side calls work, and the failure surfaces later as a 403 when the client tries to initiate a blob upload or PUT the manifest. That is why "login succeeded but push is forbidden" is a normal, expected shape of failure.
- Why are these bearer tokens given very short lifetimes, and what does the registry do to validate one?The registry validates only the JWT signature and claims against a trusted key; it does not call back to the auth service, so there is no way to revoke a token mid-life. Short TTLs — often minutes — bound the damage if a token leaks, for example through a log or a misconfigured proxy. Clients simply re-run the handshake when a token expires, which is cheap.
saying these in an interview costs you the question
- Thinking the registry keeps a session after `docker login`
- Believing the credentials themselves are sent on every registry request
- Assuming a token grants everything you asked for in `scope`
- Claiming the registry calls the auth service to validate each token
- Forwarding the `Authorization` header to the redirected blob storage URL