From 54541fa8357cf2af616effbaeb7d5b8d76f80ea2 Mon Sep 17 00:00:00 2001 From: agent Date: Sat, 7 Nov 2026 09:00:55 +0100 Subject: Public read-only distroless OCI mirror (zot + edge vhost + auditor) --- README.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 README.md (limited to 'README.md') diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9d5a60 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# distroless-mirror + +Public, read-only OCI mirror of the four Google distroless images, served from +the VPS on : + +| Pull this | Equals upstream | +| --- | --- | +| `img.dieschoenewolke.de/distroless/static:` | `gcr.io/distroless/static:` | +| `img.dieschoenewolke.de/distroless/base:` | `gcr.io/distroless/base:` | +| `img.dieschoenewolke.de/distroless/cc:` | `gcr.io/distroless/cc:` | +| `img.dieschoenewolke.de/distroless/python3:` | `gcr.io/distroless/python3:` | + +`` is one of `latest`, `nonroot`, `debug`, `debug-nonroot` — the four +multi-arch tags upstream publishes for every repo. Digest-identical mirrors of +the upstream index; `linux/amd64` and `linux/arm64` children are mirrored, the +rare architectures (arm, ppc64le, riscv64, s390x) are not. + +Why: the home Forgejo registry is VPN-only, so nothing public could pull these +images. Like the cgit mirror (`git.dieschoenewolke.de`) this one lives on the +VPS and keeps serving while the home instance is down. + +``` +docker pull img.dieschoenewolke.de/distroless/static:nonroot +docker pull img.dieschoenewolke.de/distroless/python3@sha256: +incus remote add img https://img.dieschoenewolke.de --protocol oci +``` + +## Layout + +| Path | Purpose | +| --- | --- | +| `deploy/docker-compose.yml` | VPS stack (`/root/Compose/Webserver/Registry`), zot bound to `127.0.0.1:5000` | +| `deploy/config.json` | zot config: anonymous read-only, sync from `gcr.io` | +| `deploy/nginx-edge-img.conf` | Server block for `edge-nginx` | +| `scripts/verify-mirror.sh` | Digest comparison + pull verification + marker tags | +| `.forgejo/workflows/mirror-sync.yml` | Scheduled auditor (every 6 h) | + +## Why zot and not `distribution/registry:2` + CI `skopeo copy` + +Both were on the table. `registry:2` would have made the Forgejo workflow the +only writer (`skopeo copy --all` per changed digest) and would have needed a +push credential plus an edge carve-out for that credential, or a second +VPN-only push port. + +zot wins on three points that matter here: + +1. **No push surface at all.** With no `auth` block and + `anonymousPolicy: ["read"]`, zot has no path for an authenticated write — + not from the internet, not from the LAN, not from a leaked token. With + `registry:2`, read-only can only be enforced by the edge (`limit_except GET`) + plus a separate authenticated push route; the write capability exists in the + process. +2. **Freshness does not depend on the home host.** zot pulls from gcr.io on the + VPS. A `registry:2` + CI design stops updating whenever the home Forgejo (or + its runner) is down, because the copy job runs there. +3. **`dedupe`.** The four repos share most layers; zot hardlinks identical + blobs instead of storing them per repository. `registry:2` stores blobs + globally too, but has no tag-level `dedupe` and no built-in upstream sync, so + a stale mirror is a CI bug rather than a config value. + +The cost of that choice: zot is a second non-standard registry binary to keep +updated, and the tag allowlist lives in `deploy/config.json` rather than in the +workflow. The auditor workflow covers the second half by failing when a ref we +already marked disappears. + +## Deploy + +```sh +ssh root@vpn +mkdir -p /root/Compose/Webserver/Registry +# copy deploy/docker-compose.yml, deploy/config.json +cd /root/Compose/Webserver/Registry && docker compose up -d +``` + +Edge: merge `deploy/nginx-edge-img.conf` into +`/root/Compose/Webserver/NginxEdge/nginx/default.conf` **in place** +(`cat new > default.conf` — it is a single-file bind mount), then + +```sh +docker exec edge-nginx nginx -t +docker exec edge-nginx nginx -s reload +``` + +Rollback: remove the block again (`git checkout`/`cp` the backup, write in +place, reload) and `docker compose -p registry down` in the Registry directory. +The `*.dieschoenewolke.de` wildcard certificate already covers `img`, so no +certificate change is involved. + +DNS: `A 45.142.177.86` and `AAAA 2a03:4000:4a:2ba::1` for +`img.dieschoenewolke.de` (deSEC). + +## Verify + +```sh +curl -sI https://img.dieschoenewolke.de/v2/ # 200 +docker pull img.dieschoenewolke.de/distroless/static:nonroot +docker run --rm img.dieschoenewolke.de/distroless/python3:nonroot -V +./scripts/verify-mirror.sh # digest comparison + markers +``` + +From the VPS itself the request hits CrowdSec AppSec and can be challenged — +test from another host. + +## Notes + +- The edge disables AppSec for this host (docker/skopeo user agents trip CRS + bot rules); the CrowdSec IP bouncer still applies and `limit_except GET` + rejects every write verb at the edge. +- `distribution/registry`-style `/v2/` on `git.dieschoenewolke.de` is not + usable: OCI clients derive `/` from the reference, so the same + hostname would need a path-stripping vhost and `GET /v2/` would collide with + cgit. Hence the dedicated name. -- cgit v1.2.3