2026-09-07 11:55:21 +02:00
2026-09-07 11:55:21 +02:00
2026-09-07 11:55:21 +02:00
2026-09-07 11:55:21 +02:00
2026-09-07 11:55:21 +02:00
2026-09-07 11:55:21 +02:00
2026-09-07 11:55:21 +02:00
2026-09-07 11:55:21 +02:00
2026-09-07 11:59:53 +02:00
2026-09-07 12:00:11 +02:00
2026-09-07 11:55:21 +02:00

Spiegelserver: Flatpak + DNF unter einer Domain

Ein Compose-Stack, zwei kuratierte Spiegel, ein Webserver, ein Signing-Key:

Pfad Inhalt Container
https://<domain>/flatpak/ gefilterter Flathub-Spiegel (OSTree) flatpak-mirror
https://<domain>/dnf/ RPM-Spiegel fuer alles ohne Flatpak dnf-mirror

Beide Sync-Container schreiben nur in ihr eigenes Volume und haben keinen Port nach aussen. Der gemeinsame nginx bekommt beide Volumes read-only gemountet und liefert sie unter je einem Pfad aus.

Was sich beide teilen:

  • Denselben GPG-Signing-Key - er wird extern erzeugt und beiden Containern ueber dasselbe Verzeichnis ./gpg hereingereicht. Clients muessen damit nur einem Key vertrauen, egal ob Flatpak oder RPM.
  • Dasselbe Git-Repo mit den Freigabelisten: flatpaks.yaml fuer den Flatpak-Spiegel, packages.yaml fuer den DNF-Spiegel.
  • Denselben nginx und damit dieselbe Domain und dasselbe TLS-Zertifikat.

Erste Einrichtung

cp .env.example .env

1. Signing-Key bereitstellen

Der Key wird extern erzeugt (nicht mehr vom Container) und als GnuPG-Homedir in den Ordner ./gpg gelegt. Zum Beispiel auf einer Admin-Maschine:

export GNUPGHOME=$(mktemp -d)
gpg --batch --passphrase '' --quick-generate-key 'Company Mirror <it@example.com>' rsa4096 sign never
gpg --list-secret-keys --keyid-format=long        # Fingerprint notieren
rsync -a "$GNUPGHOME/" /pfad/zum/stack/gpg/

Dann in der .env GPG_KEY_ID auf den Fingerprint setzen.

  • Der Key darf keine Passphrase haben, sonst kann unbeaufsichtigt weder rpmsign noch flatpak build-sign signieren.
  • ./gpg wird beiden Containern read-only nach /run/secrets/gpg gemountet. Jeder Container kopiert das Homedir beim Start in sein eigenes GNUPGHOME (/etc/mirror-gpg) - gpg schreibt naemlich in seinem Homedir (trustdb, random_seed, Lockfiles), und so bleibt das Original unveraendert und die beiden Container kommen sich beim Signieren nicht ins Gehege.
  • ./gpg auf dem Host nicht world-readable lassen (chmod 700) und sichern/backupen: geht der Key verloren, muessen alle Clients den neuen Public Key neu importieren - fuer beide Repos.

2. Freigabe-Git-Repo anlegen

Inhalt von sources-repo-template/ (flatpaks.yaml + packages.yaml) in ein eigenes Git-Repo pushen, anpassen und in der .env SOURCES_GIT_URL / SOURCES_GIT_REF setzen. Fuer privaten Zugriff entweder

  • Token: SOURCES_GIT_USERNAME + SOURCES_GIT_TOKEN (Read-only-PAT), oder
  • SSH: Deploy-Key nach ./git-ssh/id_ed25519 legen (chmod 600) und SOURCES_GIT_SSH_KEY=/run/secrets/git-ssh/id_ed25519 setzen.

Das Token wird nie in die Git-URL eingebettet (URLs landen in Logs), sondern per GIT_CONFIG_*-Header uebergeben - es ist damit auch nicht per ps aux auf dem Host sichtbar. Im Container-Environment und in der .env steht es trotzdem im Klartext, Dateirechte entsprechend setzen.

3. Starten

docker compose up -d --build
docker compose logs -f flatpak-mirror dnf-mirror

Der erste Lauf dauert je nach Freigabeliste laenger (Firefox/LibreOffice sind gross). Danach steht bereit:

URL Zweck
https://<domain>/flatpak/<MIRROR_REPO_ID>.flatpakrepo fertige Remote-Definition inkl. Key
https://<domain>/flatpak/<MIRROR_REPO_ID>.gpg Public Key (binaer, fuer OSTree)
https://<domain>/dnf/<MIRROR_REPO_ID>.repo die eine .repo-Datei fuer Clients
https://<domain>/dnf/RPM-GPG-KEY-mirror Public Key (ASCII-armored, fuer RPM)

Clients anbinden

# Flatpak
flatpak remote-add --if-not-exists company-mirror \
    https://<domain>/flatpak/company-mirror.flatpakrepo

# DNF
rpm --import https://<domain>/dnf/RPM-GPG-KEY-mirror
curl -o /etc/yum.repos.d/company-mirror.repo \
    https://<domain>/dnf/company-mirror.repo
pkcon refresh -c 1   # PackageKit/Discover hat einen eigenen Cache

Beide Male ist es derselbe Key - er muss nur einmal als vertrauenswuerdig eingestuft werden.

Freigaben aendern

flatpaks.yaml bzw. packages.yaml im Freigabe-Repo bearbeiten, committen, pushen. Beide Container ziehen das Repo vor jedem Sync-Lauf neu (FLATPAK_SYNC_INTERVAL_SECONDS, Default 15 Min.; DNF_SYNC_INTERVAL_SECONDS, Default 6 h). Fuer einen sofortigen Lauf:

docker compose restart flatpak-mirror   # bzw. dnf-mirror

Funktionsprinzip

Flatpak-Spiegel

  1. flatpaks.yaml aus Git holen.
  2. Fuer jede gelistete App den Ref app/<id>/x86_64/<branch> per ostree pull --mirror von Flathub holen.
  3. Aus der metadata des gepullten Refs die benoetigte Runtime auslesen und rekursiv mitspiegeln (App -> Runtime -> Basis-Runtime).
  4. Jeden Ref mit dem Spiegel-Key neu signieren (flatpak build-sign, fuer runtime/-Refs mit --runtime - ohne das sucht flatpak die ID unter app/ und die Runtime bliebe unsigniert).
  5. Refs loeschen, die weder freigegeben noch als Abhaengigkeit noetig sind.
  6. Summary + Static Deltas erzeugen und signieren; .flatpakrepo und Public Key ins Repo schreiben.

DNF-Spiegel

  1. packages.yaml aus Git holen.
  2. Pro Fremdrepo per dnf download genau die dort gelisteten Paketnamen holen (keine automatische Abhaengigkeitsaufloesung).
  3. Filtern: Pakete, deren Name AlmaLinux selbst liefert (ALMA_BASELINE_REPOIDS), werden verworfen - die Clients loesen sie aus ihren eigenen Alma-Repos auf. Ausnahmen ueber force_include.
  4. Umsignieren mit dem Spiegel-Key (rpmsign --resign), in einen privaten Cache pro Fremdrepo.
  5. Alle Fremdrepos zu einem oeffentlichen Repo zusammenfuehren - Clients binden nur dieses eine ein.
  6. createrepo_c + AppStream-Katalog (appstreamcli compose, fuer Discover/GNOME Software), repomd.xml separat signieren (repo_gpgcheck=1), Client-.repo schreiben.

Verzeichnisstruktur

docker-compose.yml       beide Spiegel + der EINE nginx
.env.example             gemeinsame Konfiguration beider Spiegel
gpg/                     extern erzeugter Signing-Key (GnuPG-Homedir),
                         read-only in BEIDE Container gemountet
git-ssh/                 optional: SSH-Deploy-Key fuers Freigabe-Repo
nginx/
  conf.d/repo.conf       ein Server-Block: /flatpak/ und /dnf/
flatpak-mirror/
  Dockerfile             debian-slim mit ostree, flatpak, git, yq
  entrypoint.sh          uebernimmt den gemeinsamen Key nach $GNUPGHOME
  gpg-import.sh          Key-Uebernahme + Signier-Selbsttest (identische
                         Kopie in dnf-mirror/, beide pflegen)
  sync.sh                Pull + Signieren + Summary + Client-Dateien
dnf-mirror/
  Dockerfile             AlmaLinux 10 mit createrepo_c, rpm-sign, appstream
  entrypoint.sh          uebernimmt den Key, droppt per setpriv auf "mirror"
  gpg-import.sh          identische Kopie aus flatpak-mirror/
  run-as-mirror.sh       Key pruefen/exportieren, Sync-Schleife
  sync.sh                Download -> Filtern -> Signieren -> Merge -> Metadaten
  alma-baseline.repo.d/  optional: eigene Baseline-Repos fuer den Abgleich
sources-repo-template/   Vorlage fuer das gemeinsame Freigabe-Git-Repo
  flatpaks.yaml
  packages.yaml

Fehlersuche: "kein geheimer Schluessel ... gefunden"

Beide Container brechen beim Start ab, wenn sie mit GPG_KEY_ID nicht signieren koennen. Die Meldung listet dann die Fingerprints, die im Schluesselbund tatsaechlich liegen - meistens passt einfach GPG_KEY_ID in der .env nicht dazu (Platzhalter aus .env.example stehengeblieben, Key-ID statt Fingerprint, Tippfehler).

Direkt auf dem Host nachsehen, was in ./gpg liegt:

gpg --homedir ./gpg --batch --list-secret-keys --keyid-format=long
  • Kommt hier sec mit einem Fingerprint: genau diesen (ohne Leerzeichen) als GPG_KEY_ID in die .env eintragen, dann docker compose up -d.
  • Kommt nur pub und kein sec: im Ordner liegt nur der oeffentliche Teil. private-keys-v1.d/ muss mindestens eine .key-Datei enthalten - sonst den Key neu erzeugen bzw. den geheimen Teil nachliefern.
  • Kommt gar nichts: der Ordner ist kein GnuPG-Homedir, sondern enthaelt z.B. eine exportierte .asc-Datei. Dann einmalig importieren:
    gpg --homedir ./gpg --import /pfad/zum/secret-key.asc
    
  • Meldet der Container "Signieren schlaegt fehl": der Key hat eine Passphrase. Unbeaufsichtigtes Signieren braucht einen Key ohne Passphrase - gpg --homedir ./gpg --edit-key <fpr> passwd und leer lassen.

Der Ordner wird beiden Containern read-only hereingereicht; sie kopieren daraus nur das Schluesselmaterial (pubring.kbx, private-keys-v1.d, trustdb.gpg, ...). Agent-Sockets (S.gpg-agent & Co.) und Lock-Dateien, die vom gpg-agent auf dem Host stammen, koennen also gefahrlos darin liegen bleiben - sie werden nicht mitkopiert.

Bekannte Einschraenkungen

  • TLS terminiert dieser Stack nicht. Entweder Zertifikate in den nginx-Container mounten und einen listen 443 ssl-Block ergaenzen, oder - meist einfacher - HTTP_PORT auf z.B. 8080 legen und einen vorhandenen Reverse Proxy davorhaengen. Da beide Repos jetzt hinter einem nginx liegen, reicht dafuer ein Zertifikat und ein Upstream.
  • Passphrase-geschuetzte Keys sind nicht unterstuetzt (siehe oben).
  • AppStream/Discover: Anwendungen ohne .desktop/.metainfo.xml bleiben per dnf/pkcon installierbar, erscheinen aber ohne Katalogeintrag in Discover. Reine CLI-Pakete lassen sich ueber console_apps: in packages.yaml trotzdem in den Katalog aufnehmen. Der DNF-Teil ist auf AlmaLinux 10+ ausgelegt (appstreamcli compose aus appstream-compose).
  • Namensbasierter Abgleich (DNF): Ein Paket wird uebersprungen, sobald Alma ein gleichnamiges hat - unabhaengig von der Version. Braucht eine Anwendung eine neuere Version, hilft force_include.
  • Ein gemeinsamer Namensraum (DNF): Zwei Fremdrepos mit gleichnamigem Paket ueberschreiben sich im gemeinsamen Repo; das zuletzt gesyncte gewinnt.
  • Git-Ausfall: Schlaegt der Fetch fehl, laeuft der DNF-Sync mit der letzten erfolgreich geholten Liste weiter (nur beim allerersten Lauf bricht er ab). Der Flatpak-Sync bricht den Lauf ab und versucht es beim naechsten Intervall erneut.
  • Migration aus den beiden Einzel-Stacks: Die alten Volumes werden nicht weiterverwendet (andere Namen, andere Pfade im Container) - beide Spiegel bauen sich beim ersten Lauf neu auf. Da jetzt ausserdem mit einem neuen, gemeinsamen Key signiert wird, muessen die Clients ohnehin einmalig neu onboardet werden.
  • Der zusammengefuehrte Stack wurde nicht gegen echte Upstreams end-to-end getestet (keine Docker-/Internet-Umgebung in dieser Session) - die Skripte sind syntaktisch geprueft, der erste docker compose up sollte beobachtet werden.
S
Description
No description provided
Readme
77 KiB
Languages
Shell 94.4%
Dockerfile 5.6%