From de96f8aa7d967c33750d1ea64704b02419d1e853 Mon Sep 17 00:00:00 2001 From: "David M. Klein" <4+leander19961@noreply.localhost> Date: Wed, 26 Aug 2026 10:06:43 +0200 Subject: [PATCH] Dateien nach "/" hochladen --- README.md | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed1547f --- /dev/null +++ b/README.md @@ -0,0 +1,327 @@ +# 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://: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:///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://:${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.