Skip to content

Commit 30c687a

Browse files
committed
docs: authenticated catalogue probes and when a digest pin rots
Two registry questions that look settled and are not. A 401 from an authenticated registry is a transport answer: it is returned for every repository, so an anonymous probe cannot tell "not published" from "not logged in". The reference shows the realm discovery, the token exchange using the credential docker login already stored, and the discipline that makes a 404 mean something -- run a positive control in the same loop, or the run is measuring your credentials rather than the catalogue. A digest pin freezes the base as well as the application, which is the point while upstream is publishing and inverts when upstream resumes rebuilding the same release. Measured on one image: 1461 fixable findings on a digest pinned ten months earlier against 293 on the floating tag it came from, same application version. The reference gives the timestamp check to run before claiming either "pinned, therefore safe" or "upstream never rebuilds". It closes with the two ways a floating-tag exemption for self-rebuilt images gets written wrongly: scoping it to a registry host rather than to ownership, and testing the tag against the literal string latest, which lets latest-rolling through. SKILL.md gains the reference row; the one-line restatement of the frontmatter description above Core Principles is dropped to stay inside the word cap. Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
1 parent cff7fcc commit 30c687a

2 files changed

Lines changed: 81 additions & 2 deletions

File tree

skills/docker-development/SKILL.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ allowed-tools:
1818

1919
# Docker Development
2020

21-
Patterns for building, testing, and deploying Docker containers.
22-
2321
## Core Principles
2422

2523
1. **Minimal** -- Alpine/distroless, multi-stage
@@ -134,3 +132,4 @@ Exclude: `.git`, `node_modules`/`vendor`, `.env*`, `*.pem`, `*.key`
134132
- `references/dind-testing-patterns.md` -- Docker-in-Docker testing patterns
135133
- `references/bind-mount-ownership.md` -- root-owned bind-mount artifacts
136134
- `references/gpg-verification.md` -- gpgv patterns; stale keybox locks
135+
- `references/registry-catalogue-and-pin-rot.md` -- catalogue probes; pin rot
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Registry Catalogue Probing and Pin Rot
2+
3+
Two questions that look settled and are not: *does this registry publish image
4+
X?* and *is a digest pin still the careful choice?*
5+
6+
## A 401 is a transport answer, not an absence
7+
8+
Authenticated registries refuse anonymous requests for **every** repository, so
9+
an anonymous probe cannot distinguish "not published" from "not logged in":
10+
11+
```bash
12+
curl -s -o /dev/null -w '%{http_code}\n' https://dhi.io/v2/mariadb/tags/list # 401
13+
curl -s -o /dev/null -w '%{http_code}\n' https://dhi.io/v2/phpmyadmin/tags/list # 401
14+
```
15+
16+
Both 401. Neither says anything about the catalogue. Ask the registry which
17+
realm it wants, then authenticate against it — the credential is already on the
18+
machine if `docker login` has run:
19+
20+
```bash
21+
curl -sI https://dhi.io/v2/mariadb/tags/list | grep -i www-authenticate
22+
# Bearer realm="https://dhi.io/token",service="registry.docker.io",scope="repository:mariadb:pull"
23+
24+
AUTH=$(jq -r '.auths["dhi.io"].auth' ~/.docker/config.json) # never echo this
25+
tok=$(curl -s -H "Authorization: Basic $AUTH" \
26+
"https://dhi.io/token?service=registry.docker.io&scope=repository:mariadb:pull" \
27+
| jq -r '.token')
28+
curl -s -o /dev/null -w '%{http_code}\n' \
29+
-H "Authorization: Bearer $tok" https://dhi.io/v2/mariadb/tags/list # 200
30+
```
31+
32+
Now 200 versus 404 discriminates, and the probe has demonstrated it can return
33+
both — which is what makes a 404 evidence. Run the positive control (an image
34+
you know exists) in the same loop as the question you are actually asking; a
35+
run that returns 404 for everything is measuring your credentials.
36+
37+
`.auths[…].auth` is base64 `user:token`, so treat the value as the secret it
38+
is: pass it through a variable, never into displayed output. With a
39+
`credsStore` configured there is no `auth` field — read the credential from the
40+
helper instead.
41+
42+
## A digest pin can rot into the worse choice
43+
44+
Pinning a third-party image to a digest freezes the application *and* the base
45+
underneath it. That is the point while upstream is publishing; it inverts the
46+
moment upstream resumes rebuilding the same release. Measured on one image
47+
during a single afternoon, counting only findings that have a fix:
48+
49+
| reference | with a fix | CRITICAL+HIGH |
50+
|---|---:|---:|
51+
| digest pinned ten months earlier | 1461 | 258 |
52+
| the floating tag it was pinned from | 293 | 59 |
53+
54+
Same application version in both. The tag moved with the rebuild; the pin did
55+
not. Before writing "pinned, therefore safe" — or the opposite, "upstream never
56+
rebuilds, so there is no update path" — read the tag's timestamp:
57+
58+
```bash
59+
curl -s "https://hub.docker.com/v2/repositories/library/<image>/tags?page_size=10&ordering=last_updated" \
60+
| jq -r '.results[] | "\(.name)\t\(.last_updated)\t\(.digest[0:19])"'
61+
```
62+
63+
Equal digests across `:latest` and `:X.Y.Z` mean the release tag *is* latest, so
64+
waiting for an upstream rebuild is not a plan. A moved `last_updated` means any
65+
claim resting on staleness has expired and needs re-measuring, not repeating.
66+
67+
## Floating tags are correct for images you rebuild
68+
69+
The pinning rule is about trust, not about syntax: an image your own CI rebuilds
70+
daily should float, because a pin pins the fix out too. Two failure modes when
71+
that exemption is written down:
72+
73+
- **Scoping it to a registry host.** Ownership is what earns the exemption, not
74+
which host serves the bytes. An organisation publishing to both a private
75+
registry and `ghcr.io/<org>` needs both recognised — compared as path
76+
components, never as substrings, or `ghcr.io/someone/<org>-lookalike` and
77+
`evil.com/<your-registry>/x` qualify.
78+
- **Testing the tag against the literal string `latest`.** `latest-rolling`,
79+
`latest-alpine` and friends float exactly as much and sail through. Treat a
80+
`latest-*` or `edge-*` prefix as floating.

0 commit comments

Comments
 (0)