Files
dnf-repo-test/README.md
T

328 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RPM Mirror Sync
`sync.sh` spiegelt einzeln benannte Pakete aus Drittanbieter-RPM-Repos in ein
lokales Repo, ohne dabei Pakete mitzuspiegeln, die AlmaLinux selbst schon
liefert (BaseOS, AppStream, Extras, CRB/"Extras for Enterprise"). Nur die in
der YAML-Liste explizit genannten Pakete bekommen AppStream-Metadaten und
werden damit in GNOME Software / Discover als installierbar angezeigt.
## Voraussetzungen (auf dem Server, der das Skript ausführt)
```
dnf install -y dnf-plugins-core createrepo_c python3-pyyaml git
```
Das Skript muss als root laufen (schreibt kurzzeitig eine `.repo`-Datei nach
`/etc/yum.repos.d/`).
## Konfiguration
Alles über Umgebungsvariablen, z.B. in der systemd-Unit oder im Cron-Job:
| Variable | Bedeutung | Default |
|---------------------|------------------------------------------------------------------|---------|
| `SOURCES_GIT_REPO` | Git-Repo mit der YAML-Liste (leer = lokale Datei verwenden) | |
| `SOURCES_GIT_BRANCH` | Branch im Git-Repo | `main` |
| `SOURCES_YAML_FILE` | Pfad zur YAML-Datei (im Git-Repo oder lokal) | `sources.yaml` |
| `WORK_DIR` | Arbeitsverzeichnis (Downloads, Logs, Zwischenlisten) | `/var/cache/rpm-mirror-sync` |
| `REPO_BASE_DIR` | Zielverzeichnis der gespiegelten Repos | `/srv/repo` |
| `CLIENT_REPO_DIR` | Wo fertige `.repo`-Dateien für Clients abgelegt werden | `${REPO_BASE_DIR}/client-repos` |
| `CLIENT_BASE_URL` | URL, unter der `REPO_BASE_DIR` für Clients erreichbar ist | `http://mirror.example.local/repo` |
| `ALMA_REPO_IDS` | repoids der lokalen AlmaLinux-Repos (`dnf repolist`) | `baseos appstream extras crb` |
**Wichtig:** `ALMA_REPO_IDS` muss zu den tatsächlichen repoids auf dem
Server passen (`dnf repolist` prüfen) — sonst funktioniert der
Ausschluss-Mechanismus nicht korrekt.
## Ausführen
```bash
export SOURCES_GIT_REPO=https://git.example.com/infra/rpm-sources.git
export CLIENT_BASE_URL=http://repo.internal.example.com/repo
./sync.sh
```
Danach liegt unter `REPO_BASE_DIR/pkgs/` das eine kombinierte Repo mit allen
Paketen aller Quellen (per Webserver auszuliefern) und unter
`CLIENT_REPO_DIR/mirror.repo` die eine fertige `.repo`-Datei, die auf die
Clients verteilt wird (z.B. via Ansible/Config-Management nach
`/etc/yum.repos.d/`).
Am besten per systemd-Timer (z.B. täglich) statt Cron, damit Logs sauber in
`journalctl` landen.
## Wie der Ausschluss von AlmaLinux-Abhängigkeiten funktioniert
1. Für jedes Source-Repo wird zunächst abgefragt, welche Pakete *dieses*
Repo überhaupt bereitstellt (`dnf repoquery` nur gegen das Source-Repo).
2. Anschließend wird die komplette Abhängigkeitskette der gewünschten
Pakete aufgelöst — dabei sind sowohl das Source-Repo als auch die
lokalen AlmaLinux-Repos aktiv, damit die Auflösung überhaupt gelingt
(z.B. wenn ein Paket von `glibc` abhängt).
3. Von den heruntergeladenen RPMs wird alles verworfen, was nicht in der
Liste aus Schritt 1 steht — also alles, was tatsächlich aus einem
AlmaLinux-Repo kam. Übrig bleiben nur RPMs, die es ausschließlich im
Drittanbieter-Repo gibt.
**Wichtig: "übrig bleiben" ist meist mehr als nur die Namen unter
`packages:`.** Wenn ein angefordertes Paket von weiteren Drittanbieter-Paketen
abhängt, die AlmaLinux nicht bereitstellt (z.B. bei .NET: `dotnet-host`,
`dotnet-hostfxr-*`, `dotnet-apphost-pack-*`, `aspnetcore-runtime-*`, ...),
werden die zwangsläufig mitgespiegelt — sonst schlägt `dnf install
dotnet-sdk-8.0` auf dem Client mit einer fehlenden Abhängigkeit fehl, die
nirgendwo sonst verfügbar wäre. Nur echte AlmaLinux-Pakete werden
ausgeschlossen, nicht Drittanbieter-Abhängigkeiten. Im Log ist das ab jetzt
pro Paket sichtbar:
```
[...] keeping (requested): dotnet-sdk-8.0-8.0.130-1.el10_2.x86_64
[...] keeping (dependency): dotnet-host-10.0.11-1.el10_2.x86_64
[...] Source 'dotnet': kept 26 package(s) - 6 explicitly requested + 20 pulled-in third-party dependencies (...)
```
### Sonderfall: `baseurl` zeigt auf ein AlmaLinux-eigenes Repo
`dnf` "sucht" nie selbstständig bei Drittanbietern — es lädt ausschließlich
von der URL, die ihr unter `baseurl` eintragt. Zeigt diese URL zufällig auf
ein AlmaLinux-eigenes Repo (z.B. `.../AppStream/...`), ist also gar keine
externe Quelle im Spiel. Genau das ist bei .NET auf AlmaLinux 10 der Fall:
`dotnet-sdk`, `dotnet-runtime`, `dotnet-host` & Co. sind Teil des offiziellen
AppStream-Repos (anders als bei älteren Alma-/RHEL-Versionen mit separatem
Microsoft-Repo).
Konsequenz: Die "requested"-Pakete sind hier technisch schon über das
normale, auf jedem Client ohnehin aktivierte AppStream-Repo installierbar —
der Ausschluss-Mechanismus (Schritt 1-3 oben) erkennt das nicht, weil er nur
gegen die *anderen* Alma-Repos (BaseOS, Extras, CRB) vergleicht, nicht gegen
die Quelle selbst. Ein normaler Mirror-Lauf würde hier also RPM-Inhalte
duplizieren, die AlmaLinux bereits liefert.
**Für genau diesen Fall gibt es `metadata_only: true`** (pro Quelle in der
YAML, siehe `sources.example.yaml`): Es werden **keine RPMs** heruntergeladen,
verifiziert oder umsigniert — nur kuratierte AppStream-Metadaten für die
gelisteten Paketnamen werden generiert und veröffentlicht. Das tatsächliche
Installieren übernimmt `dnf` weiterhin ganz normal über das Repo, das das
Paket wirklich bereitstellt (bei AlmaLinux-eigenen Paketen also das
Standard-AppStream-Repo, das auf jeder Standardinstallation ohnehin aktiv
ist). Der Effekt: Discover zeigt nur eure kuratierte Auswahl statt des
kompletten (oft sehr großen) AppStream-Kataloges, ohne dass ihr Inhalte
dupliziert, die es schon gibt.
**Voraussetzung:** Die Clients müssen das Repo, das die Pakete tatsächlich
bereitstellt, bereits aktiviert haben — bei Standard-AlmaLinux-Installationen
(BaseOS/AppStream/Extras/CRB aktiv) ist das automatisch der Fall.
Für ein echtes Drittanbieter-Repo (Pakete, die AlmaLinux nirgends hat) lasst
ihr `metadata_only` einfach weg (Default: `false`) — dann läuft der volle
Mirror-Prozess wie oben beschrieben, inklusive Verifikation und Umsignieren.
## Umsignieren mit einem eigenen GPG-Key
Damit eure Clients nicht für jede gespiegelte Quelle einen eigenen GPG-Key
importieren müssen, signiert `sync.sh` jedes gespiegelte Paket auf einen
einzigen, mirror-eigenen Schlüssel um:
1. Beim ersten Lauf wird — falls noch keiner unter `GPG_HOME` existiert —
einmalig ein GPG-Schlüsselpaar erzeugt (unattended, ohne Passphrase; das
ist für automatisierte Repo-Signierung üblich). Der öffentliche Teil wird
nach `REPO_BASE_DIR/RPM-GPG-KEY-mirror` exportiert.
2. Für jede Quelle wird — sofern `gpgkey:` in der YAML gesetzt ist — das
heruntergeladene RPM zunächst gegen den **originalen** Key des Upstream
geprüft (`rpmkeys --checksig`). Schlägt die Prüfung fehl, bricht das
Skript für diese Quelle ab (kein stillschweigendes Mirroring manipulierter
Pakete). Ist kein `gpgkey:` angegeben, wird eine Warnung geloggt und die
Prüfung übersprungen.
3. Danach wird die ursprüngliche Signatur entfernt und durch eine Signatur
mit dem mirror-eigenen Key ersetzt (`rpmsign --resign`).
4. Alle generierten Client-`.repo`-Dateien verweisen auf denselben Key
(`${CLIENT_BASE_URL}/RPM-GPG-KEY-mirror`) — unabhängig davon, wie viele
Quellen gespiegelt werden.
**`gpgcheck` und `repo_gpgcheck`:** Die generierte `.repo`-Datei setzt beide
auf `1`:
- `gpgcheck=1` prüft beim Installieren die Signatur **jedes einzelnen RPM**
gegen den Mirror-Key.
- `repo_gpgcheck=1` prüft zusätzlich die Signatur der **Repo-Metadaten
selbst** (`repodata/repomd.xml.asc`, ein detached signature). Ohne das
könnte jemand mit Schreibzugriff auf den Webserver/die Leitung
theoretisch die Paketliste manipulieren (z.B. ein älteres, verwundbares
Paket als aktuell ausgeben oder Einträge entfernen), ohne dass eine
einzelne RPM-Signatur berührt würde — `repo_gpgcheck` schließt genau diese
Lücke.
`sync.sh` signiert `repomd.xml` nach jedem Lauf neu (nachdem `createrepo_c`
und `modifyrepo_c` fertig sind, da beide die Datei noch verändern) mit
demselben Mirror-Key, den auch die Pakete tragen — Clients brauchen also
weiterhin nur den einen `RPM-GPG-KEY-mirror`.
**Wichtig zur Aufbewahrung des privaten Schlüssels:** `GPG_HOME`
(Default: `${WORK_DIR}/gnupg`) enthält den privaten Signierschlüssel und darf
**niemals** über nginx/den Webserver erreichbar sein — im mitgelieferten
Docker-Setup liegt er bewusst auf dem `sync-cache`-Volume, das *nicht* in den
`nginx`-Container gemountet wird, nur `repo-data` (die öffentlichen Dateien)
ist das. Wer Lesezugriff auf `GPG_HOME` hat, kann Pakete als euer Mirror
signieren — entsprechend schützen (Backups, Zugriffsrechte).
`GPG_HOME` muss auf persistentem Speicher liegen: wird es gelöscht, erzeugt
das Skript beim nächsten Lauf einen neuen Schlüssel, und alle Clients müssen
den neuen öffentlichen Key erneut importieren.
### Revocation-Zertifikat
Da der Mirror-Key mit `expire=never` erzeugt wird, gibt es kein automatisches
Ablaufdatum, über das ein kompromittierter Key von selbst "entschärft" würde.
Deshalb erzeugt `sync.sh` beim allerersten Lauf zusammen mit dem Schlüssel
auch ein **Revocation-Zertifikat** unter `GPG_HOME/revocation-cert.asc` — das
ist die einzige Möglichkeit, den Key später für ungültig zu erklären, falls
der private Schlüssel jemals kompromittiert wird.
**Direkt nach dem ersten Start aus dem Container herausholen und offline
sichern:**
```bash
docker compose cp syncer:/var/cache/rpm-mirror-sync/gnupg/revocation-cert.asc \
./mirror-key-revocation-cert.asc
```
(Pfad anpassen, falls ihr `WORK_DIR`/`GPG_HOME` in `.env` geändert habt.)
Das Zertifikat danach an einem Ort außerhalb des Docker-Hosts aufbewahren
(Passwort-Safe, Offline-Backup o.ä.) — nicht im selben Ordner wie der
private Key selbst, sonst bringt es im Kompromittierungsfall nichts. Das
Skript loggt den Hinweis beim ersten Lauf zusätzlich in `sync.log`.
Kommt der Ernstfall, importiert ihr das Zertifikat auf einer Maschine mit
Zugriff auf `GPG_HOME` (`gpg --homedir ... --import mirror-key-revocation-cert.asc`),
exportiert den nun als widerrufen markierten Public Key erneut nach
`REPO_BASE_DIR/RPM-GPG-KEY-mirror` und verteilt ihn an alle Clients — dnf/rpm
lehnen mit einem widerrufenen Schlüssel signierte Pakete danach ab.
## Wie die Sichtbarkeit in Discover/GNOME Software gesteuert wird
`createrepo_c` erzeugt Repo-Metadaten für **alle** im Zielverzeichnis
liegenden RPMs (nötig, damit `dnf install` Abhängigkeiten auflösen kann).
GNOME Software/Discover zeigt aber nur Pakete an, für die es
AppStream-Komponenten-Metadaten gibt. Diese werden vom Skript **nur** für
die explizit unter `packages:` gelisteten Namen erzeugt (nicht für
mitgezogene Abhängigkeiten) und per `modifyrepo_c --mdtype=appstream` in
die Repo-Metadaten injiziert.
**Einschränkung:** Es werden nur "generic" AppStream-Komponenten erzeugt
(kein Icon, keine Screenshots, keine `.desktop`-Datei — die liefert das
Originalpaket nicht mit). Das reicht, damit ein Paket in Discover
installierbar ist und in der Suche auftaucht, aber es erscheint **nicht**
auf der kuratierten "Entdecken"-Startseite von GNOME Software — das ist ein
Verhalten von GNOME Software/appstreamcli, das sich über Repo-Metadaten
allein nicht ändern lässt.
## Repo auf Clients einbinden
Alle Quellen landen in **einem einzigen** kombinierten Repo unter
`REPO_BASE_DIR/pkgs/` — egal wie viele Einträge eure `sources.yaml` hat.
Entsprechend gibt es auch nur **eine** Client-`.repo`-Datei:
`CLIENT_REPO_DIR/mirror.repo` (Default: `/srv/repo/client-repos/mirror.repo`,
über nginx erreichbar unter `/repo/client-repos/mirror.repo`).
**Einbinden auf einem Client — ein Kommando, ein Repo:**
```bash
curl -fsSL http://<host>:8080/repo/client-repos/mirror.repo \
-o /etc/yum.repos.d/rpm-mirror.repo
dnf makecache
```
Das war's — egal ob ihr später weitere Quellen zur `sources.yaml`
hinzufügt, die Client-Konfiguration bleibt unverändert (`baseurl` und
`gpgkey` ändern sich nicht, nur der Inhalt des Repos wächst). Für die
Verteilung an viele Clients eignet sich dasselbe `curl`-Kommando in
Ansible/Config-Management oder einem Kickstart-Postscript.
`https://<host>/repo` selbst (ohne `/pkgs/`) ist weiterhin **kein** dnf-Repo,
sondern nur ein Verzeichnis-Listing (nginx `autoindex`) — `baseurl` muss auf
`/repo/pkgs/` zeigen (das steht schon so in der generierten
`mirror.repo`, ihr müsst daran nichts anpassen).
**Namenskonflikte:** Da jetzt alle Quellen in ein gemeinsames Verzeichnis
spiegeln, dürfen zwei Quellen keine RPMs mit demselben Paketnamen aber
unterschiedlichem Inhalt liefern (identische NEVRA aus zwei Quellen ist
unproblematisch, das ist einfach dasselbe Paket). Achtet bei mehreren
Drittanbieter-Repos in einer `sources.yaml` auf eindeutige Paketnamen.
## Dateien
- `sync.sh` — Hauptskript
- `sync_helpers.py` — YAML-Parsing & AppStream-XML-Generierung (muss im
selben Verzeichnis wie `sync.sh` liegen)
- `sources.example.yaml` — Beispiel-Quellenliste
- `docker-compose.yml`, `.env.example`, `docker/` — Container-Setup (siehe unten)
## Docker-Setup
Zwei Container:
- **syncer** — AlmaLinux 10 Image, führt `sync.sh` beim Start und danach alle
`SYNC_INTERVAL_SECONDS` erneut aus (einfache Sleep-Loop, kein Cron/systemd
im Container). Schreibt nach `/srv/repo` (benanntes Volume `repo-data`).
- **nginx** — liefert `repo-data` read-only unter `http://<host>:${NGINX_PORT}/repo/`
mit Directory-Listing aus, inklusive `client-repos/` für die fertigen
`.repo`-Dateien.
### Start
```bash
cp .env.example .env
# .env anpassen: mindestens CLIENT_BASE_URL und ALMA_REPO_IDS prüfen
docker compose up -d --build
docker compose logs -f syncer
```
**`.env` ist die einzige Konfigurationsquelle:** `docker-compose.yml` selbst
enthält keine hartkodierten Umgebungsvariablen oder Defaults mehr — beide
Container laden alles per `env_file: .env`. Wer etwas ändern will (Pfade,
Sync-Intervall, Client-URL, Signier-Identität, Port, ...), bearbeitet
ausschließlich `.env` und macht `docker compose up -d`, ohne `docker-compose.yml`
anzufassen.
Danach ist das (eine, kombinierte) Repo erreichbar unter
`http://localhost:8080/repo/pkgs/` und die eine Client-`.repo`-Datei unter
`http://localhost:8080/repo/client-repos/mirror.repo`.
### Eigene Quellenliste einbinden
**Lokale Datei (Standard):** `LOCAL_SOURCES_YAML` in `.env` auf den Pfad
eurer echten `sources.yaml` setzen (relativ zum `docker-compose.yml`), z.B.
`LOCAL_SOURCES_YAML=./sources.yaml`. Wird read-only nach
`/config/sources.yaml` im Container gemountet.
**Git-Repo statt lokaler Datei:** `SOURCES_GIT_REPO`, `SOURCES_GIT_BRANCH`
und `SOURCES_YAML_FILE` (Pfad *innerhalb* des Git-Repos, relativ) in `.env`
setzen. Der Bind-Mount aus der lokalen Variante wird dann schlicht ignoriert.
### Einmaliger Sync statt Dauerlauf
`RUN_ONCE=true` in `.env` setzen — der `syncer`-Container synct dann einmal
und beendet sich, statt in der Schleife zu laufen. Praktisch, wenn ihr den
Sync lieber über einen externen Scheduler (Kubernetes CronJob, systemd-Timer
mit `docker compose run`, etc.) anstoßen wollt.
### ALMA_REPO_IDS prüfen
Die repoids im Container können von einer bare-metal-Installation abweichen.
Nach dem ersten Start prüfen:
```bash
docker compose exec syncer dnf repolist
```
und `ALMA_REPO_IDS` in `.env` entsprechend anpassen, dann
`docker compose up -d` erneut ausführen.
### Persistenz
- `repo-data` (Volume) — die gespiegelten Repos selbst; das ist auch, was
nginx ausliefert.
- `sync-cache` (Volume) — Arbeitsverzeichnis von `sync.sh`
(`WORK_DIR`, u.a. Logs unter `sync.log`).
Beide überleben `docker compose down`; erst `docker compose down -v` löscht
sie.