Read-only mirror — the public repositories of the Forgejo instance, refreshed every 10 minutes. The live instance (fg.dieschoenewolke.de) is reachable only over the VPN; clone here with https://git.dieschoenewolke.de/git/<owner>/<repo>.git.
summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md112
1 files changed, 112 insertions, 0 deletions
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 <https://img.dieschoenewolke.de>:
+
+| Pull this | Equals upstream |
+| --- | --- |
+| `img.dieschoenewolke.de/distroless/static:<tag>` | `gcr.io/distroless/static:<tag>` |
+| `img.dieschoenewolke.de/distroless/base:<tag>` | `gcr.io/distroless/base:<tag>` |
+| `img.dieschoenewolke.de/distroless/cc:<tag>` | `gcr.io/distroless/cc:<tag>` |
+| `img.dieschoenewolke.de/distroless/python3:<tag>` | `gcr.io/distroless/python3:<tag>` |
+
+`<tag>` 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:<index digest>
+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 `<host>/<repo>` from the reference, so the same
+ hostname would need a path-stripping vhost and `GET /v2/` would collide with
+ cgit. Hence the dedicated name.