From 12d480edd2d342aba53b55555918a7012ae54f53 Mon Sep 17 00:00:00 2001 From: "David M. Klein" <4+leander19961@noreply.localhost> Date: Thu, 3 Sep 2026 14:39:56 +0200 Subject: [PATCH] Dateien nach "/" hochladen --- README.md | 303 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2d9c8c --- /dev/null +++ b/README.md @@ -0,0 +1,303 @@ +# DNF-Spiegelserver fuer Pakete ohne Flatpak + +Ergaenzt den bestehenden Flathub-Filter-Spiegel um einen zweiten Weg fuer +Anwendungen, die es nur als RPM gibt. Beide sollen unter einer Domain laufen: + +- `https:///flatpak/...` - bereits vorhanden +- `https:///dnf/...` - dieser Stack + +## Funktionsprinzip + +Der `dnf-mirror`-Container laeuft dauerhaft und fuehrt alle +`SYNC_INTERVAL_SECONDS` (Default 6h) folgenden Zyklus aus: + +0. **packages.yaml aktualisieren**: `git fetch`/`reset --hard` gegen das in + `SOURCES_GIT_URL` konfigurierte Git-Repo, das eine Datei `packages.yaml` + enthaelt (siehe `sources-repo-template/`). Das passiert vor **jedem** + Sync-Lauf - neue/geaenderte Eintraege in diesem separaten Repo wirken also + spaetestens beim naechsten automatischen Lauf, ganz ohne Neubau/Neustart + des Containers. `packages.yaml` listet Fremdrepos mit ihrer URL und genau + den Paketnamen, die daraus geholt werden sollen, z.B.: + ```yaml + repos: + - id: vscode + baseurl: https://packages.microsoft.com/yumrepos/vscode + gpgkey: https://packages.microsoft.com/keys/microsoft.asc + packages: + - code + ``` + +Danach, pro dort definiertem Fremdrepo, zunaechst in einen PRIVATEN, +internen Zwischenspeicher (noch nicht oeffentlich sichtbar): + +1. **`dnf download`** holt aus dem Fremdrepo genau die unter `packages:` + gelisteten Paketnamen (keine automatische Abhaengigkeitsaufloesung, keine + sonstigen Pakete aus diesem Repo - siehe `sources-repo-template/README.md`). +2. **Filtern**: Jedes heruntergeladene Paket wird trotzdem gegen die + Paketnamen geprueft, die AlmaLinux selbst liefert (BaseOS/AppStream/ + Extras/CRB/EPEL - konfigurierbar ueber `ALMA_BASELINE_REPOIDS`). Ist der + Name dort vorhanden, wird das Paket verworfen: die Clients haben ihre + Alma-Repos ja selbst schon eingebunden und loesen es von dort auf (Schutz + gegen versehentliches Doppelspiegeln, falls jemand faelschlich ein + Alma-Paket in `packages.yaml` eintraegt). Ausnahmen lassen sich im + `force_include`-Feld von `packages.yaml` eintragen (z.B. wenn eine + Anwendung eine neuere Version einer Bibliothek braucht als Alma mitbringt). +3. **Umsignieren**: Die verbleibenden, tatsaechlich zu spiegelnden Pakete + werden mit `rpmsign --resign` komplett neu signiert - die Original- + Herstellersignatur wird ersetzt. Dadurch brauchen Clients nur **einen** + GPG-Key (den des Spiegelservers), egal von wie vielen verschiedenen + Herstellern die gespiegelten Pakete urspruenglich stammen. + +Erst danach, EINMALIG ueber alle Fremdrepos zusammen: + +4. **Zusammenfuehren**: Die signierten Pakete aller Fremdrepos werden zu + **einem gemeinsamen, oeffentlichen Repo** gemergt (`rsync --delete` ueber + alle privaten Zwischenspeicher hinweg in `/srv/dnf`). Ein Fremdrepo, das + in einem Sync-Lauf mal nicht erreichbar war, behaelt einfach seinen + letzten erfolgreichen Stand im gemeinsamen Repo; wird ein Fremdrepo oder + ein einzelnes Paket aus `packages.yaml` entfernt, verschwindet es beim + naechsten Lauf auch aus dem gemeinsamen Repo. +5. **Metadaten bauen**: `createrepo_c` fuer die eigentlichen Repo-Metadaten + des gemeinsamen Repos, dazu AppStream-Katalogdaten (`appstreamcli compose`, + siehe unten), damit die Anwendungen mit Name/Icon/Beschreibung in + **Discover (GNOME Software)** auftauchen statt nur per `dnf`/`pkcon` auf + der Kommandozeile sichtbar zu sein. +6. `repomd.xml` wird zusaetzlich separat signiert (`repomd.xml.asc`), damit + Clients `repo_gpgcheck=1` nutzen koennen (schuetzt auch die Metadaten + selbst, nicht nur die einzelnen Pakete). +7. Eine fertige Client-`.repo`-Datei wird nach `/srv/dnf/$MIRROR_REPO_ID.repo` + geschrieben und ist unter `https:///dnf/$MIRROR_REPO_ID.repo` + abrufbar - das ist das **einzige** Repo, das Clients einbinden muessen. + +Der `nginx`-Container bekommt das Ausgabe-Volume read-only gemountet und +liefert es unter `/dnf/` aus. Er baut keine Pakete, sondern ist nur fuer den +Zugriff von aussen zustaendig. + +## Erste Einrichtung + +```sh +# 1. Paketliste: sources-repo-template/packages.yaml (angepasst) in ein +# EIGENES, separates Git-Repo pushen (siehe sources-repo-template/README.md) +cp sources-repo-template/packages.yaml /pfad/zum/eigenen/git-repo/packages.yaml +# Inhalt pruefen/anpassen (eigene Fremdrepos + Paketnamen eintragen), dann +# committen+pushen ins neue Git-Repo + +# 2. Compose-Stack konfigurieren +cp .env.example .env +# .env anpassen: mindestens MIRROR_DOMAIN, MIRROR_REPO_ID, GPG_NAME, GPG_EMAIL, +# SOURCES_GIT_URL + +# 3. Bei SSH-Zugriff aufs Git-Repo: Deploy-Key (nur Lesezugriff) ablegen +cp /pfad/zum/deploy-key ./git-ssh/id_ed25519 +chmod 600 ./git-ssh/id_ed25519 + +docker compose up -d --build +docker compose logs -f dnf-mirror # ersten Sync-Lauf beobachten +``` + +Der `./gpg`-Ordner neben der `docker-compose.yml` wird beim ersten Start vom +Container selbst befuellt (GPG-Schluesselpaar) - dort ist nichts manuell +vorzubereiten, aber der Ordner sollte auf dem Host entsprechend geschuetzt +(nicht world-readable) und **gesichert/gebackupt** werden: geht er verloren, +muessen alle Clients den neuen Public Key neu importieren. + +Nach dem ersten Lauf: + +- `https:///dnf/RPM-GPG-KEY-mirror` - der Public Key des Spiegels +- `https:///dnf/company-mirror.repo` (bzw. `$MIRROR_REPO_ID.repo`) - + die eine Repo-Datei, die Clients einbinden +- `https:///dnf/` - das gemeinsame Repository selbst, enthaelt die + Pakete aus allen in `packages.yaml` konfigurierten Fremdrepos + +## Neue Anwendungen spiegeln + +Fuer jede Anwendung ohne Flatpak einen Eintrag unter `repos:` in der +`packages.yaml` **im separaten Paketlisten-Git-Repo** ergaenzen (Fremdrepo- +URL + die gewuenschten Paketnamen unter `packages:`), committen, pushen - der +Container zieht das beim naechsten Sync-Lauf automatisch. Kein Zugriff auf +den Docker-Host/Compose-Stack noetig, das kann also z.B. per Merge-Request- +Review an ein anderes Team delegiert werden. + +## Clients anbinden + +Einmalig (per Ansible/Kickstart/Intune-Skript/Image-Build - vermutlich schon +Teil eurer bestehenden Entra-ID-Provisioning-Pipeline), z.B. mit dem +mitgelieferten Skript: + +```sh +./client/bootstrap.sh software.example.org +``` + +Das importiert den Mirror-GPG-Key und legt die **eine** `.repo`-Datei ab - +Clients sehen darin von Anfang an alle gespiegelten Anwendungen aus allen +konfigurierten Fremdrepos, ganz ohne pro Anwendung ein weiteres Repo +einbinden zu muessen. Rootlose Installation/Update/Deinstallation laeuft +danach ueber die bereits bestehenden Polkit-Regeln fuer PackageKit wie +gewohnt - hier ist nichts zusaetzlich noetig. + +## Bekannte Einschraenkungen + +- **AppStream/Discover**: `appstreamcli compose` extrahiert AppStream-Metadaten + aus Paketen, die selbst `.metainfo.xml`/`.desktop`-Dateien mitbringen. Bringt + eine Anwendung so etwas nicht mit, erscheint sie trotzdem installierbar, + aber ohne Katalogeintrag (Name/Icon/Beschreibung) in Discover - nur per + Suche nach dem Paketnamen. Das ist eine Eigenschaft des Quellpakets, keine + Konfigurationsluecke. + - Der Stack ist auf **AlmaLinux 10 und neuer** ausgelegt. Gebaut wird der + Katalog mit `appstreamcli compose` (Pakete `appstream` + + `appstream-compose` - Letzteres ist ein eigenes Subpaket, ohne das + `appstreamcli compose` zur Laufzeit mit "AppStream Compose binary ... was + not found" scheitert). Da dieses Kommando auf einem Verzeichnisbaum statt + auf `.rpm`-Dateien arbeitet, entpackt `sync.sh` vorher die relevanten + Pfade (`usr/share/{applications,metainfo,icons,pixmaps}`) aus allen + Paketen in so einen Baum. + - **Format der beiden repodata-Eintraege ist heikel.** Auf dem Client holt + PackageKit die repodata-Typen `appstream` und `appstream-icons` und reicht + sie an `as_utils_install_metadata_file()` von libappstream weiter, das sie + nach `/var/cache/swcatalog/{xml,icons}/` installiert - erst von dort lesen + Discover und `appstreamcli`. Diese Funktion ist bei Dateinamen sehr + waehlerisch, und ein Fehlschlag laesst den **kompletten** Repo-Refresh + scheitern (nicht nur die Icons). Deshalb schreibt `sync.sh` beides + ausdruecklich mit `modifyrepo_c --compress-type=gz`: + - Der Katalog muss auf `.xml.gz` enden. `modifyrepo_c` komprimiert die + uebergebene Datei neu und nutzt auf AlmaLinux 10 per Default **zstd**; + das dort installierte AppStream 1.0.2 kennt `.xml.zst` aber noch nicht + (erst ab 1.0.5) und lehnt die Datei mit "Can not process files of this + type" ab. + - Der Icon-Tarball muss auf `.tar.gz` enden **und** eine bekannte Groesse + (`48x48`, `64x64`, `128x128`, jeweils optional `@2`) im Dateinamen + tragen, sonst gibt es "Unable to find valid icon size in icon tarball + name". Er wird flach nach + `/var/cache/swcatalog/icons///` entpackt, darf also nur + die PNGs auf oberster Ebene enthalten. PackageKit holt genau einen + `appstream-icons`-Eintrag, `sync.sh` liefert deshalb genau eine Groesse + aus (bevorzugt 64x64). + - `appstreamcli compose` schreibt den Origin nur in Datei- und + Verzeichnisnamen, nicht als `origin=`-Attribut in den Katalog. + libappstream liest ihn aber genau daraus und findet ohne ihn die Icons + nicht - `sync.sh` traegt ihn deshalb nachtraeglich ein. + - **jpackage-basierte Java/JavaFX-Apps** (u.a. XPipe): Diese legen ihre + `.desktop`-Datei typischerweise NICHT unter `/usr/share/applications/` + ab, sondern direkt im eigenen Installationsverzeichnis (z.B. + `/opt/xpipe/xpipe.desktop`) und verschieben sie erst per `%post`-Skript + dorthin - das sieht `sync.sh` beim reinen Lesen des Pakets nicht. Der + `sync.sh` durchsucht deshalb zusaetzlich das gesamte Paket nach + `.desktop`/`.metainfo.xml`/`.appdata.xml`-Dateien ausserhalb der + Standardpfade und holt genau diese gezielt nach. + - **Reine Kommandozeilen-Pakete** (`dotnet-sdk`, `terraform`, `kubectl`, ...) + bringen weder `.desktop`- noch `.metainfo.xml`-Dateien mit und sind in + Discover deshalb standardmaessig **gar nicht auffindbar** - auch nicht ueber + den exakten Paketnamen, denn Discovers PackageKit-Backend durchsucht + ausschliesslich den AppStream-Pool (per `dnf`/`pkcon` bleiben sie natuerlich + installierbar). Wer solche Pakete im Katalog haben will, traegt ihren + Paketnamen in `console_apps:` der `packages.yaml` ein; `sync.sh` erzeugt + dann eine `console-application`-Komponente aus dem RPM-Header + (`%{SUMMARY}`, `%{DESCRIPTION}`, `%{URL}`, `%{LICENSE}`) plus + `` fuer die Befehle aus `/usr/[s]bin`. Details siehe + `sources-repo-template/README.md`. Bewusst opt-in, damit nicht saemtliche + Runtime- und Bibliotheks-Unterpakete als "Anwendungen" im Katalog landen. + - **Fehlende `.metainfo.xml` + ``**: Hat eine Anwendung nur eine + `.desktop`-Datei, aber keine `.metainfo.xml` (der Normalfall bei + unabhaengigen Drittanbieter-RPMs), erzeugt `sync.sh` selbst eine minimale + Ersatz-Metainfo daraus (Name/Beschreibung/Icon/Kategorien aus der + `.desktop`-Datei). `appstreamcli compose` (AppStream 1.0+) verlangt dafuer + zwingend eine ``, sonst schlaegt die betroffene Komponente + mit `description-missing` fehl. Ausserdem braucht JEDE Komponente - auch + eine echte, vom Hersteller mitgelieferte `.metainfo.xml` - ein + ``-Element, damit PackageKit sie einem installierbaren RPM + zuordnen kann; Upstream-Metainfo-Dateien haben das so gut wie nie (kennen + den Distro-Paketnamen nicht), `sync.sh` ergaenzt es deshalb automatisch + beim Einsammeln der Dateien. + - Ist keins von beiden installiert, schlaegt die Erzeugung fehl, oder + bringt das Paket auch an keinem dieser Orte eine `.desktop`-/AppStream- + Datei mit, bleiben Pakete ganz normal per `dnf`/`pkcon` installierbar, + erscheinen aber ohne Katalogeintrag in Discover (siehe Log-Warnung). +- **PackageKit-Cache auf dem Client**: `dnf makecache` aktualisiert nur den + Cache des interaktiven `dnf`-Kommandos. PackageKit (und damit Discover) + haelt einen davon getrennten eigenen Metadaten-Cache und aktualisiert ihn + per Default nicht zuverlaessig von selbst zeitnah. `client/bootstrap.sh` + ruft deshalb zusaetzlich `pkcon refresh -c 1` auf - ohne das kann ein neu + eingebundenes Repo (oder neu hinzugekommene Pakete darin) in Discover trotz + korrekter AppStream-Daten verzoegert oder gar nicht auftauchen. +- **Namensbasierter Abgleich**: Paket X wird uebersprungen, sobald ein Paket + gleichen Namens in Alma existiert - unabhaengig von der Version. Falls eine + gespiegelte Anwendung eine neuere Version einer Alma-Bibliothek als + Abhaengigkeit braucht, dafuer `force_include` in `packages.yaml` nutzen. +- **Keine Abhaengigkeitsaufloesung**: `packages.yaml` listet die zu + spiegelnden Pakete explizit; `dnf download` holt genau diese Namen aus dem + jeweiligen Fremdrepo, ohne fehlende Abhaengigkeiten automatisch + nachzuladen. Fehlt einer gespiegelten Anwendung eine Bibliothek, die weder + aus Alma/EPEL noch aus demselben Fremdrepo kommt, muss sie explizit mit in + `packages:` aufgenommen werden. +- **Ein gemeinsames Repo, ein gemeinsamer Namensraum**: Alle Fremdrepos aus + `packages.yaml` werden zu einem einzigen Repo zusammengefuehrt. Zwei + verschiedene Fremdrepos mit einem gleichnamigen Paket (gleicher + .rpm-Dateiname) landen deshalb im selben Speicherplatz - das zuletzt + gesyncte gewinnt. Bei der Zusammenstellung von `packages.yaml` also darauf + achten, dass sich die gewaehlten Anwendungen nicht ueberschneiden. +- **GPG-Key-Ablage**: Der Signing-Key liegt (statt in einem anonymen Docker- + Volume) im Ordner `./gpg` direkt neben der `docker-compose.yml` - dadurch + laesst er sich mit normalen Host-Mitteln sichern/backupen/einschraenken. + Der Container startet dafuer intern kurz als root, chownt `./gpg` auf den + unprivilegierten `mirror`-User und wechselt dann per `setpriv` dauerhaft zu + diesem (siehe `mirror/entrypoint.sh`) - der eigentliche Sync inkl. `git`/ + `dnf`/`gpg` laeuft nie als root. +- **GPG-Passphrase**: Der Signing-Key wird standardmaessig ohne Passphrase + erzeugt, damit der Container vollautomatisch signieren kann. Der Schutz + liegt dann in den Dateirechten von `./gpg` (nicht world-readable lassen!). + Eine Passphrase ist in `run-as-mirror.sh` fuer die Schluesselerzeugung + vorbereitet, automatisches Signieren mit passphrasegeschuetztem Key ist + aber NICHT fertig verdrahtet (braucht zusaetzlich `gpg-preset-passphrase` + o.ae.) - fuer den unbeaufsichtigten Betrieb wird die leere Passphrase + empfohlen. +- **Git-Zugriff**: `SOURCES_GIT_URL` wird vor jedem Sync-Lauf gefetcht. Schlaegt + das fehl (Netzwerk, Auth), laeuft der Sync mit der letzten erfolgreich + geholten Paketliste weiter (nicht beim allerersten Lauf - dort bricht der + Sync-Zyklus ohne erreichbares Git-Repo komplett ab). Nur ein Lesezugriff + (Deploy-Key/Read-only-Token) ist noetig. +- **TLS**: Der `nginx`-Service ist bewusst schlank gehalten (Port 80). Fuer + TLS entweder Zertifikate mounten und einen `listen 443 ssl` Server-Block + ergaenzen, oder - passend zum Wunsch "eine Domain fuer beide Repos" - + diesen Stack hinter einem bereits vorhandenen Reverse Proxy/Loadbalancer + betreiben, der auch den Flatpak-Spiegel terminiert. +- **Upgrade von einer aelteren Version dieses Stacks** (mit einem Repo PRO + Fremdrepo unter `/dnf//`): Das Ausgabeformat hat sich grundlegend + geaendert (jetzt ein gemeinsames Repo direkt unter `/dnf/`). Vor dem + ersten Start mit dieser Version das `dnf-repo-data`-Volume einmalig leeren + (`docker compose down` + Volume-Inhalt loeschen), damit keine alten + `/`-Verzeichnisse und `.repo`-Dateien liegen bleiben, und + anschliessend allen Clients die neue, eine `.repo`-Datei ausrollen. +- Dieser Aufbau wurde nicht gegen echte Internet-Upstreams end-to-end + getestet (keine Docker-Umgebung/Internetzugriff in dieser Session + verfuegbar) - Skripte sind syntaktisch geprueft, der erste `docker compose + up` sollte aber mit echten Repo-Definitionen beobachtet werden. + +## Verzeichnisstruktur + +``` +docker-compose.yml +.env.example +gpg/ (leer, wird vom Container befuellt) - Signing-Key, + auf dem Host sichern/schuetzen +git-ssh/ optional: SSH-Deploy-Key fuers Paketlisten-Git-Repo +mirror/ + Dockerfile AlmaLinux-10-Image mit dnf-plugins-core, createrepo_c, + rpm-sign, appstream+appstream-compose, + git, python3-pyyaml, jq + entrypoint.sh laeuft als root: chownt ./gpg, wechselt per setpriv + zu "mirror" + run-as-mirror.sh laeuft als "mirror": GPG-Key-Erzeugung/-Export, + Sync-Schleife + sync.sh Kernlogik: packages.yaml aus Git holen -> je + Fremdrepo dnf download -> filtern -> signieren + (privater Cache) -> alle Fremdrepos zu EINEM Repo + zusammenfuehren -> Metadaten -> Client-.repo + config/ + alma-baseline.repo.d/ optional: eigene Baseline-Repos fuer den Abgleich +sources-repo-template/ Vorlage fuer das SEPARATE Git-Repo mit der + Paketliste (packages.yaml) +nginx/ + conf.d/repo.conf liefert /dnf/... aus, Platzhalter fuer /flatpak/... +client/ + bootstrap.sh einmaliges Client-Onboarding (Key-Import + .repo) +```