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
blob: f9d5a60d92a8f06f842795b5efc75a7f23286c35 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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.