How would you query a registry's HTTP API v2 directly with curl to see which manifest a tag currently resolves to and whether a given layer blob exists, and when is doing that worth the trouble?
answer
- Challenge → token → Bearer on every call
- Accept header lists manifest media types
- Docker-Content-Digest = the image digest
- HEAD blob → 200 exists / 404 missing
- Blob GET may 307 to storage; don't forward auth
basics
~20 sGet a bearer token, then GET /v2/<repo>/manifests/<tag> with an Accept header listing acceptable manifest media types — the response body plus the Docker-Content-Digest header give you the manifest and digest. HEAD /v2/<repo>/blobs/<digest> reports whether a blob exists.
solid answer
~50 sThree calls cover most investigations. 1. **Token**: hit any `/v2/` endpoint, read the `WWW-Authenticate` challenge, exchange credentials at the realm for a scoped bearer token. 2. **Manifest**: `GET /v2/team/app/manifests/1.2` with `Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json`. The `Accept` header matters — omit it and older registries return a legacy schema. The response's `Docker-Content-Digest` header is the image digest; the body is either a per-platform manifest or an index listing platform manifests you then fetch by digest. 3. **Blob**: `HEAD /v2/team/app/blobs/sha256:...` → 200 means present, 404 means missing. Use `GET` on the config blob to read the image's env, entrypoint and history without pulling. It is worth doing when a pull fails ambiguously, when you need to prove which digest a tag pointed at, when you suspect a proxy or mirror is rewriting responses, when checking whether a mirror has replicated content, or when you want image metadata without downloading gigabytes of layers. `HEAD` on a manifest is also the cheapest existence check for a tag.
code
bash · 23 linesREG=registry.example.com; REPO=team/app; TAG=1.2
# 1. Scoped pull token
TOKEN=$(curl -s -u "$USER:$SECRET" \
"https://auth.example.com/token?service=$REG&scope=repository:$REPO:pull" | jq -r .token)
# 2. What does the tag resolve to?
curl -s -D - -o manifest.json \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.oci.image.index.v1+json" \
-H "Accept: application/vnd.oci.image.manifest.v1+json" \
-H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
"https://$REG/v2/$REPO/manifests/$TAG" | grep -i docker-content-digest
# 3. Is a specific layer present?
curl -sI -H "Authorization: Bearer $TOKEN" \
"https://$REG/v2/$REPO/blobs/sha256:ab12..." | head -1
# 4. Read image metadata without pulling layers
CFG=$(jq -r '.config.digest' manifest.json)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://$REG/v2/$REPO/blobs/$CFG" | jq '.config.Entrypoint, .config.Env'go deeper
Know that a registry is a plain HTTP API and that a tag resolves to a manifest which lists layer digests.
Perform the token exchange, request a manifest with proper Accept headers, and read the digest and layer list from the response.
Use the API to localise failures — token service versus registry, manifest versus blob — verify mirrors and promotions by digest, and handle redirect and proxy pitfalls safely.
Treat direct API access as an operational and forensic capability: proving what digest was deployed, auditing replication, and understanding what enumeration endpoints expose.
## Why go under the CLI The CLI collapses a multi-step HTTP conversation into one line and one error message. When something misbehaves — an intermittent 403, a mirror serving stale content, a proxy mangling headers, a tag that seems to have moved — talking to the API directly tells you exactly which step fails and what the server actually said. It also lets you read image metadata without transferring layers, which is valuable when the image is large or the environment is bandwidth-constrained. ## Step 1: the token Every request needs authorization. Probe the base endpoint and read the challenge: ``` curl -sI https://registry.example.com/v2/ Www-Authenticate: Bearer realm="https://auth.example.com/token",service="registry.example.com" ``` Then exchange credentials for a scoped token: ``` curl -s -u "$USER:$SECRET" \ "https://auth.example.com/token?service=registry.example.com&scope=repository:team/app:pull" ``` The JSON contains `token`. Anonymous access to a public repository works with no `-u`. If this step fails, the problem is authentication; if it succeeds and the registry still refuses, the problem is authorization — that split alone resolves a large share of registry tickets. ## Step 2: the manifest, and the `Accept` header ``` curl -s -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/vnd.oci.image.index.v1+json" \ -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \ -H "Accept: application/vnd.oci.image.manifest.v1+json" \ -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ -D headers.txt \ https://registry.example.com/v2/team/app/manifests/1.2 ``` Content negotiation is real here: the registry serves whichever supported type you accept, and a request without these headers can get a legacy schema or an error. Two response shapes: - **An index / manifest list** — `manifests[]` entries each with a `digest`, `size` and `platform` (`os`, `architecture`, sometimes `variant`). This is a multi-platform image; fetch the entry for the platform you care about **by digest** using the same endpoint. - **An image manifest** — a `config` descriptor and an ordered `layers[]` array, each with `mediaType`, `size` and `digest`. The response header `Docker-Content-Digest` is the digest of the manifest the tag currently resolves to. That is the value you record when you need to prove what was deployed, and comparing it across two registries or two moments in time answers "did this tag change?". `HEAD` on the same URL returns the headers only — the cheapest way to ask "does this tag exist and what does it point to?". ## Step 3: config and blobs The config blob is small and contains the interesting metadata: ``` curl -s -H "Authorization: Bearer $TOKEN" \ https://registry.example.com/v2/team/app/blobs/sha256:<config-digest> | jq '.config.Env, .history' ``` That gives environment variables, entrypoint, exposed ports, labels, the `diff_ids` of the uncompressed layers and the build history — all without pulling the image. For layers, `HEAD /v2/<repo>/blobs/<digest>` answers existence with `200` plus `Content-Length`, or `404`. Note that a blob `GET` commonly returns `307` to object storage with a pre-signed URL; follow redirects with `-L`, and **do not** forward the `Authorization` header to the redirect target (curl does not by default across hosts, which is the correct behaviour — the signed URL carries its own auth and leaking a bearer token to a storage host is a real incident class). ## Other useful endpoints - `GET /v2/` — API version check and the auth challenge; a 200 means "registry reachable and I am authorized at base level". - `GET /v2/<repo>/tags/list` — tags in a repository, paginated with `?n=` and a `Link` header. - `GET /v2/_catalog` — repository listing; usually restricted or disabled on shared registries because it is an enumeration surface. - `DELETE /v2/<repo>/manifests/<digest>` — deletes by digest, never by tag, and only when the registry has deletion enabled; the space returns after garbage collection. ## When it earns its keep - An ambiguous pull failure: you can see precisely whether the 401 came from the token service or the registry, and whether the manifest or a blob is missing. - Verifying a mirror or pull-through cache has the same digest for a tag as the upstream. - Confirming a promotion actually moved the digest you approved. - Reading labels, entrypoint or base-image metadata for an image you are not allowed to (or do not want to) run. - Diagnosing corporate proxies that strip `WWW-Authenticate`, rewrite `Accept`, or break the redirect to blob storage. Modern tooling (`crane`, `skopeo`, `oras` and friends) wraps these calls, and reaching for them is entirely legitimate — but knowing the underlying requests is what lets you interpret their output and debug the cases they do not cover.
- Why does the manifest request need an explicit `Accept` header?Manifests come in several media types — the OCI image manifest and index, and the older Docker v2 manifest and manifest list — and the registry performs content negotiation. If you send no `Accept`, the server may return a legacy schema or refuse, which makes a multi-platform image look like something else entirely. Listing all the types you can handle is what the real client does.
- You GET a layer blob and receive a 307 redirect. What should you be careful about?The redirect usually points at object storage with a pre-signed URL that carries its own authorization in the query string. You must not forward the registry `Authorization` header to that host, both because it is unnecessary and because leaking a bearer token to a third-party endpoint is a genuine exposure. Well-behaved clients drop auth headers on cross-host redirects, and proxies that rewrite or strip the redirect are a common cause of broken pulls.
saying these in an interview costs you the question
- Requesting a manifest with no Accept header and being surprised by the schema returned
- Expecting to delete an image by tag rather than by manifest digest
- Assuming `/v2/_catalog` is always available on a shared registry
- Forwarding the bearer token to the redirected blob storage URL
- Believing a 401 on the manifest request always means bad credentials rather than a missing token exchange