Debian-Pakete hosten
In diesem Artikel beschreibe ich, wie ich selbst ein Repo betreibe. Ich bin kein Experte auf dem Gebiet, deswegen erfolgt das nachmachen dieser Anleitung selbstverständlich auf eigene Gefahr. Außerdem behandeln wir hier das Thema TLS/SSL nicht.
Voraussetzungen
Betriebssystem
Ein Debian-Repository kann auf einer Vielzahl von Systemen gehostet werden, aber die naheliegendste Wahl ist natürlich Debian oder ein Debian-basiertes System zu verwenden. Ich benutze dafür Ubuntu Server, welches auch das System ist, mit dem folgende Anleitung getestet wurde.
Software
Hier werden nur Programme aufgelistet, welche in einer Standardinstallation von Ubuntu Server (22.04 als LXC) nicht enthalten sind.
- nginx: eine Webserver-Software
- gpg: GNU Privacy Guard - erzeugen und prüfen elektronischer Signaturen.
- bzip2: ein Komprimierungsprogramm
- tree: rekursives Directorylisting - nicht wirklich essenziell, aber wir können uns schön eine Verzeichnisstruktur anzeigen lassen
- dpkg-dev: Debian package development tools (wir verwenden im Speziellen dpkg-architecture)
Um alles zu installieren, führe folegnden Befehl aus:
sudo apt install nginx gpg bzip2 tree dpkg-dev
Repo vorbereiten (als root)
Unprivilegierten Benutzer erstellen
Hier wird der Benutzername repo verwendet, der natürlich gegen jeden anderen Namen getauscht werden kann - jedenfalls ist der Text so copy-and-paste-freundlich.
adduser
Die einfachste Art, einen neuen Benutzer anzulegen, ist mit dem Befehl adduser
. Dabei wird der neue Account interaktiv eingerichtet.
sudo adduser repo
useradd
Alternativ gibt es auch den sehr ähnlichen Befehl useradd
, dieser ist im Gegensatz zu adduser
nicht interaktiv.
sudo useradd -m --shell /bin/bash repo sudo passwd repo
Wurzelverzeichnis des Repos anlegen
Das hier gewählte Verzeichnis kann wieder frei benannt werden, aber repo bietet sich natürlich wieder gut an.
sudo mkdir /var/www/repo sudo chown repo:www-data /var/www/repo sudo chmod 755 /var/www/repo
Principle of Least Privilege
Wir haben uns in den letzten zwei Schritten an das Principle of least privilege gehalten, da der repo-User kein Mitglied der Gruppe www-data ist, und die Gruppe www-data keine Schreibrechte in unserem Repo-Wurzelverzeichnis hat. Außerdem ist der repo-User kein Mitglied der sudo-Gruppe.
Falls diese Vorgehensweise für deinen speziellen Fall nicht angebracht ist, musst du die Rechte bei dir anders vergeben. Wenn du z.B. den repo-User der Gruppe www-data hinzufügen willst, kannst du das mit sudo usermod -aG www-data repo
machen.
nginx einrichten
Wir erstellen eine Konfigurationsdatei für nginx, die wir wieder mal repo nennen. Außerdem disablen wir die Default-Welcome-Page von nginx (vorher bitte checken, ob diese Seite auch angezeigt wurde - dann wissen wir zumindest, dass nginx soweit funktioniert).
sudo rm /etc/nginx/sites-enabled/default sudo nano /etc/nginx/sites-available/repo
Folgenden Text in die Datei einfügen:
server { listen 80 default_server; server_name _; # besser die tatsächliche Domain angeben und nicht default_server verwenden # listen 80; # server_name repo.example.com; location / { # das Wurzelverzeichnis (bei Bedarf anpassen) root /var/www/repo; # autoindex on;: Erlaubt das Auflisten von Verzeichnissen. autoindex on; # Content-Disposition "inline" stellt sicher, dass die Inhalte im Browser angezeigt werden können. add_header Content-Disposition "inline"; # Der Cache-Control-Header teilt dem Client und den Proxies mit, dass die # Inhalte nicht zwischengespeichert oder wiederverwendet werden sollen. add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0"; # expires_off setzt den HTTP Expires-Header auf eine Zeit in der Vergangenheit, # was bedeutet, dass der Inhalt bereits als abgelaufen betrachtet wird. expires off; # default_type text/plain;: Setzt den Standardinhaltstyp auf text/plain. default_type text/plain; } }
Als nächstes verlinken wir die Datei in das sites-enabled Verzeichnis, testen ob die Konfiguration nicht fehlerhaft ist lassen nginx die Konfiguration ünernehmen (oder wir starten nginx gleich neu - reload durch restart ersetzen):
cd /etc/nginx/sites-enabled/ sudo ln -s ../sites-available/repo sudo nginx -t sudo systemctl reload nginx
Repo vorbereiten (als unprivilegierter Repo-User)
GPG-Schlüssel
Hier wird nur das Erstellen eines neuen Schlüssels behandelt. Trotzdem sei hier kurz erwähnt: Falls ein bestehender privater Schlüssel existiert, lässt sich dieser mit gpg --import <DATEI>
importieren.
Einen neuen GPG-Schlüssel erstellen
Führe folgenden Befehl aus und folge den Anweisungen:
gpg --full-gen-key
Folgende Auswahl ist nicht verkehrt:
- Schlüsseltyp: 1 - RSA and RSA (default)
- Schlüssellänge: 4096
- Ablaufdatum: 0 - kein Ablaufdatum (default)
- Benutzerinformationen: Diese Informationen sind im Schlüssel enthalten und helfen anderen, den Schlüssel zu identifizieren.
- Name: Ein Name aus mindestens 5 Zeichen
- E-Mail-Adresse: Optional
- Kommentar: Optional
- Passwort: Wähle ein sicheres Passwort für deinen Schlüssel. Du wirst es benötigen, wenn du den Schlüssel zum Signieren von Dateien verwendest.
Falls es ein Berechtigungsproblem gibt: Falls ein Fehler wie gpg: agent_genkey failed: Permission denied ausgegeben wird, könnte das damit zusammenhängen, dass der Benutzer nicht Eigentümer der Terminal-Datei ist (z.B. /dev/pts/5). Wenn ihr euch über SSH direkt mit dem Benutzer anmeldet, sollte das nicht passieren. Wenn ihr jedoch den Benutzer mit z.B. sudo su - repo
wechselt, wird das passieren. Wenn ihr den SSH-Zugang für euren Repo-User nicht gesperrt habt, meldet euch per SSH direkt an. Ansonsten hier die Hack-Lösung:
Mit dem Befehl tty
ermittelt ihr die entsprechende Terminaldatei für den Repo-User. Dann legt ihr als root (bzw als ein privilegierter sudo-User) den Repo-User temporär als Eigentümer fest: sudo chown repo:tty /dev/pts/<N>
. Wenn ihr mit gpg fertig seid, den Eigentümer einfach wieder auf den ursprünglichen Eigentümer zurücksetzen.
Privaten GPG-Schlüssel sichern
Wenn man sein Repo länger betreiben will, sollte man den privaten GPG-Schlüssel exportieren und sicher speichern (z.B. in einem CryFS). Mit dem folgenden Befehl lässt sich der Schlüssel im Binärformat exportieren:
gpg --output dateiname.gpg --export-secret-key <name oder ID>
Wer den Key oldschool auf Papier sichern will, kann den Key auch im Textformat exportieren
gpg --armor --output dateiname.asc --export-secret-key <name oder ID>
Verzeichnisstruktur
Ich weiche mit meiner gewählten Verzeichnisstruktur deutlich von den Debian-Standards ab, aber das ist ja das gute bei seinem eigenen Repo: Wir können es organisieren wie wir wollen - Naja, zumindest einen Teil davon ;-)
In unserem Wurzelverzeichnis legen wir uns für das eigentliche Repo ein eigenes Unterverzeichnis an, in diesem Fall deb. Somit können wir bei Bedarf unter anderen Pfaden noch andere Resourcen anbieten, die nicht mit diesem Repo zusammenhängen.
Hier gibt es dann zwei weitere Unterverzeichnisse, wobei eines doch bestimmten Regeln folgen muss, um vom Paketmanager gefunden werden zu können.
/var/www/repo/deb/dists
Die Struktur dieses Verzeichnisses ist essenziell und hat folgendes Schema:
/var/www/repo/deb/dists/ ├── distro (z.B. kinetic)/ │ ├── component (z.B. stable oder main) │ │ ├── binary-all │ │ ├── z.B. binary-amd64 │ │ ├── z.B. binary-i386 │ │ ├── ev. andere architekturen │ │ └── source │ ├── component2/ (z.B. testing) │ │ └── ... │ └── weitere-komponenten ├── distro2 (z.B. lunar)/ │ └── distro2-komponenten │ └── ... └── weitere-distros
Wer schon mal in eine sources.list-Datei gesehen hat, dem dürfte dieses Schema bekannt vorkommen. Hier ein einfaches Beispiel:
deb http://repo.example.com/deb/ lunar main testing
- deb: Diese Quelle enthält Binärpakete (im Gegensatz zu Quelltextpaketen, die mit deb-src gekennzeichnet sind).
- http://repo.example.com/deb/: Die URL des Repositories.
- lunar: Die Distribution, für welche dieses Repository vorgesehen ist.
- main testing: Die Komponenten innerhalb des Repositories. Zum Beispiel könnte main die stabilen Pakete enthalten, während testing Pakete enthält, die noch getestet werden.
/var/www/repo/deb/pool
Die Struktur dieses Verzeichnisses kann frei gewählt werden. Ich habe mich für folgendes Schema entschieden:
/var/www/repo/deb/pool/ ├── component1 (z.B. main)/ │ ├── distro1 (z.B. kinetic) │ │ ├── paket1 │ │ │ ├── version1 │ │ │ ├── version2 │ │ │ └── ... │ │ ├── paket2 │ │ │ └── ... │ │ └── ... │ ├── distro2 (z.B. lunar) │ │ └── ... │ ├── ev. andere distros ... │ │ └── ... │ ├── all (plattformunabhängige Binärpakete) │ │ └── ... │ ├── source-all (plattformunabhängige Quellpakete) │ │ └── ... │ └── source-bin (plattformabhängige Quellpakete) │ └── ... ├── component2/ (z.B. testing) │ ├── distro1 (z.B. kinetic) │ │ └── ... │ └── weitere-distros │ └── ... └── weitere-komponenten └── weitere-distros └── ...
Skripte erstellen
Folgende Dateien einfach im home-Verzeichnis des repo-Users erstellen und die entsprechenden Texte einfügen.
repo_config.sh
In dieser Datei legen wir die wichtigen Konstanten fest, welche von den anderen Skripten zur Ausführung benötigt werden.
Dieses Skript muss nicht ausführbar sein, da wir es nur in andere Skripte einbinden.
# Konstanten, die von scanrepo.sh benutzt werden # Pfad zum Wurzelverzeichnis des Repo-Webservers readonly REPO_PATH="/var/www/repo/deb" # Pfad zur Config-Datei, die die gemeinsame Config für apt-ftparchive enthält readonly APTFTP_CONF="/home/repo/aptftp-common.conf" # Der Name der *.list- und *.asc-Datei ohne Endung für das Zielsystem # ("foobar" -> /etc/apt/sources.list.d/foobar.list) # ("foobar" -> /etc/apt/trusted.gpg.d/foobar-deb-repo.asc) readonly TARGET_APT_LIST_NAME="foobar" # Die URL, unter der das Repository erreichbar ist (inkl Protokoll, z.B. http://) readonly REPO_URL="http://repo.example.com/deb" # Die ID des Schlüssels aus dem GPG-Schlüsselbund # readonly GPG_KEY_ID="$(cat /home/repo/key_id.txt)" readonly GPG_KEY_ID="<ID>" # Durch Leerzeichen getrennte Liste. Es kann natürlich auch nur ein Wert angegeben # werden. distribute-setup-repo-scripts.sh verwendet den ersten Wert readonly COMPONENTS="main testing" # durch die Option '-r' ist auch dieses Mapping readonly declare -rA DISTRO_ARCH_MAP=( # Debian 11 ["bullseye"]="arm64 armhf" # Ubuntu 22.10 ["kinetic"]="amd64 i386" # Ubuntu 23.04 ["lunar"]="amd64 i386" )
check_repo_config.sh
Dateien, die repo_config.sh benutzen, prüfen mit diesem Skript, ob die Konfiguration keine groben Fehler enthält.
Dieses Skript muss nicht ausführbar sein, da wir es nur in andere Skripte einbinden.
############################################### # Überprüfung der Konstanten aus repo_config.sh ############################################### readonly CONFIG_HINT=" Bitte überprüfe repo_config.sh." # Nicht gesetzte Variablen zulassen, um unsere eigenen Tests durchzuführen set +u if [ -z "$REPO_PATH" ]; then echo "Fehler: REPO_PATH ist nicht gesetzt.$CONFIG_HINT" exit 1 elif [ ! -d "$REPO_PATH" ]; then echo "Fehler: Es existiert kein Verzeichnis $REPO_PATH (REPO_PATH).$CONFIG_HINT" exit 1 fi if [ -z "$APTFTP_CONF" ]; then echo "Fehler: APTFTP_CONF ist nicht gesetzt.$CONFIG_HINT" exit 1 elif [ ! -f "$APTFTP_CONF" ]; then echo "Fehler: Es existiert keine Datei $APTFTP_CONF (APTFTP_CONF).$CONFIG_HINT" exit 1 fi if [ -z "$TARGET_APT_LIST_NAME" ]; then echo "Fehler: TARGET_APT_LIST_NAME ist nicht gesetzt.$CONFIG_HINT" exit 1 fi if [ -z "$REPO_URL" ]; then echo "Fehler: REPO_URL ist nicht gesetzt.$CONFIG_HINT" exit 1 fi if [ -z "$GPG_KEY_ID" ]; then echo "Fehler: GPG_KEY_ID ist nicht gesetzt.$CONFIG_HINT" exit 1 fi if ! gpg --list-keys "$GPG_KEY_ID" &>/dev/null; then echo "Fehler: Der GPG-Schlüssel mit der ID $GPG_KEY_ID konnte nicht gefunden werden." exit 1 fi if [ -z "$COMPONENTS" ]; then echo "Fehler: COMPONENTS ist nicht gesetzt.$CONFIG_HINT" exit 1 fi if [[ "$(declare -p DISTRO_ARCH_MAP 2>/dev/null)" != "declare -A"* ]]; then echo "Fehler: DISTRO_ARCH_MAP ist kein gültiges assoziatives Array.$CONFIG_HINT" exit 1 fi if [[ ${#DISTRO_ARCH_MAP[@]} -eq 0 ]]; then echo "Fehler: DISTRO_ARCH_MAP ist leer.$CONFIG_HINT" exit 1 fi for key in "${!DISTRO_ARCH_MAP[@]}"; do if [[ -z "${DISTRO_ARCH_MAP[$key]}" ]]; then echo "Fehler: Keine Architektur(en) für die Distribution $key definiert.$CONFIG_HINT" exit 1 fi done # Eine durch Leerzeichen getrennte Liste der gültigen Architekturen erstellen ALL_ARCHS=$(dpkg-architecture --list-known | tr "\n" " ") # Überprüfe die Architekturen for distro in "${!DISTRO_ARCH_MAP[@]}"; do for arch in ${DISTRO_ARCH_MAP[$distro]}; do if [[ ! " $ALL_ARCHS " =~ " $arch " ]]; then echo "Fehler: Architektur $arch für Distribution $distro ist nicht gültig." exit 1 fi done done # Nicht gesetzte Variablen sollen ab jetzt wieder einen Fehler auslösen set -u
scanrepo.sh
Dieses Skript sollte ausführbar sein, dass wir es bequemer ausführen können (chmod 700 scanrepo.sh
).
#!/bin/bash # -e: Beende das Skript bei einem Fehler # -u: Beende das Skript, wenn eine nicht gesetzte Variable benutzt wird # -o pipefail: Beende das Skript, wenn ein Kommando in einer Pipeline fehlschlägt set -euo pipefail error_handler() { local exit_code="$?" echo "Das Skript wurde durch einen Fehler in Zeile $1 mit dem Fehlercode $exit_code beendet" exit $exit_code } # Wenn das Skript durch einen Fehler abgebrochen wird, führe error_handler($LINENO) aus trap 'error_handler $LINENO' ERR # Einbinden der Konfigurationsdatei source ./repo_config.sh # Überprüfung der Konstanten aus repo_config.sh source ./check_repo_config.sh # Funktion, die ähnliche Aufrufe für apt-ftparchive abstrahiert do_apt_ftparchive_common() { local append="$1" local dist="$2" local comp="$3" local archs="$4" local search_in="$5" local out_file="$6" local cmd="$7" local extra_options=() if [ "$#" -ge 8 ]; then # Alle zusätzlichen Argumente ab dem 8. Parameter, falls vorhanden extra_options=("${@:8}") fi # Optionen, die immer gesetzt werden local all_options=( -c "$APTFTP_CONF" -o "APT::FTPArchive::Release::Codename=$dist" -o "APT::FTPArchive::Release::Suite=$comp" -o "APT::FTPArchive::Release::Architectures=$archs" -o "APT::FTPArchive::Release::Components=$comp" ) # Füge extra_options hinzu, falls vorhanden for opt in "${extra_options[@]}"; do all_options+=(-o "$opt") done if [[ "$append" == true ]]; then # füge die Ausgabe von apt-ftparchive and out_file an apt-ftparchive "${all_options[@]}" "$cmd" "$search_in" >> "$out_file" else # Überschreiben/neu anlegen der Datei apt-ftparchive "${all_options[@]}" "$cmd" "$search_in" > "$out_file" fi } # Erstelle eine Packages-Index-Datei do_packages() { do_apt_ftparchive_common false "$@" packages } # Erstelle eine Sources-Index-Datei do_sources() { do_apt_ftparchive_common true "$@" sources "APT::FTPArchive::Release::SourceComponent=$comp" } # Erstelle die Release-Datei do_release() { do_apt_ftparchive_common false "$@" release } # Erstelle komprimierte Varianten einer Datei do_compress() { local file="$1" gzip -c "$file" > "$file.gz" bzip2 -c "$file" > "$file.bz2" } # Das pool-Verzeichnis für die entsprechende Distro nach Paketen durchsuchen do_scan_repo() { local dist="$1" local dist_dir="dists/$dist" local dist_archs=${DISTRO_ARCH_MAP["$dist"]} # Suche Pakete für die entsprechende Repo-Komponente for comp in $COMPONENTS; do dist_comp_bin_all_dir="$dist_dir/$comp/binary-all" dist_comp_src_dir="$dist_dir/$comp/source" deb_comp_all_dir="pool/$comp/all" src_all_comp_dir="pool/$comp/source-all" src_bin_comp_dir="pool/$comp/source-bin" dist_comp_bin_all_file="$dist_comp_bin_all_dir/Packages" dist_comp_src_file="$dist_comp_src_dir/Sources" mkdir -p "$dist_comp_bin_all_dir" mkdir -p "$dist_comp_src_dir" mkdir -p "$deb_comp_all_dir" mkdir -p "$src_all_comp_dir" mkdir -p "$src_bin_comp_dir" # Erstelle die Packages-Index-Datei für plattformunabhängige Pakete do_packages "$dist" "$comp" "all" "$deb_comp_all_dir" "$dist_comp_bin_all_file" # Erstelle komprimierte Varianten der Sources-Index-Datei do_compress "$dist_comp_bin_all_file" # Lösche den Inhalt der Sources-Index-Datei (oder erstelle die Datei) echo "" > "$dist_comp_src_file" # Hänge die Quellen für plattformunabhängige Pakete an die Sources-Index-Datei an do_sources "$dist" "$comp" "all" "$src_all_comp_dir" "$dist_comp_src_file" # Hänge die Quellen für plattformabhängige Pakete an die Sources-Index-Datei an do_sources "$dist" "$comp" "$dist_archs" "$src_bin_comp_dir" "$dist_comp_src_file" # Erstelle komprimierte Varianten der Sources-Index-Datei do_compress "$dist_comp_src_file" # Suche nach Paketen für eine bestimmte Architektur for arch in $dist_archs; do dist_comp_bin_arch_dir="$dist_dir/$comp/binary-$arch" dist_comp_bin_arch_file="$dist_comp_bin_arch_dir/Packages" deb_comp_dist_arch_dir="pool/$comp/$dist/$arch" mkdir -p "$dist_comp_bin_arch_dir" mkdir -p "$deb_comp_dist_arch_dir" # Erstelle die Packages-Index-Datei für die entsprechende Architektur do_packages "$dist" "$comp" "$arch" "$deb_comp_dist_arch_dir" "$dist_comp_bin_arch_file" # Erstelle komprimierte Varianten der Packages-Index-Datei do_compress "$dist_comp_bin_arch_file" done done # Erstelle die Release-Datei do_release "$dist" "$comp" "$dist_archs all" "$dist_dir" "$dist_dir/Release" # GPG (GNU Privacy Guard) wird verwendet, um die Release-Datei zu signieren. # --default-key $GPG_KEY_ID: Verwendet den Schlüssel mit der angegebenen ID zum Signieren. # --yes: Automatische Bestätigung aller Abfragen (überspringt Benutzereingabeaufforderungen). # -b: Erstellt eine abgetrennte Signatur (die Signatur wird in einer separaten Datei gespeichert). # -a: Erstellt eine ASCII-armored-Signatur (die Ausgabe wird textfreundlich gemacht). # -o "$dist_dir/Release.gpg": Gibt an, wo die Signatur gespeichert werden soll. # "$dist_dir/Release": Die Datei, die signiert werden soll. gpg --default-key $GPG_KEY_ID --yes -bao "$dist_dir/Release.gpg" "$dist_dir/Release" # GPG wird verwendet, um die Release-Datei mit einer klaren Signatur zu signieren. # --default-key $GPG_KEY_ID: Verwendet den Schlüssel mit der angegebenen ID zum Signieren. # --yes: Automatische Bestätigung aller Abfragen. # --clear-sign: Erstellt eine klare Signatur (die signierte Datei wird zusammen mit der Signatur gespeichert). # --output "$dist_dir/InRelease": Gibt an, wo die signierte Datei gespeichert werden soll. # "$dist_dir/Release": Die Datei, die signiert werden soll. gpg --default-key $GPG_KEY_ID --yes --clear-sign --output "$dist_dir/InRelease" "$dist_dir/Release" } # Wechsle in das Wurzelverzeichnis des Repo-Webservers cd "$REPO_PATH" # Suche nach Paketen für die konfigurierten Distros for distro in "${!DISTRO_ARCH_MAP[@]}"; do do_scan_repo $distro done
make_repo_tree.sh
Dieses Skript sollte ausführbar sein, dass wir es bequemer ausführen können (chmod 700 scanrepo.sh
).
#!/bin/bash # -e: Beende das Skript bei einem Fehler # -u: Beende das Skript, wenn eine nicht gesetzte Variable benutzt wird # -o pipefail: Beende das Skript, wenn ein Kommando in einer Pipeline fehlschlägt set -euo pipefail error_handler() { local exit_code="$?" echo "Das Skript wurde durch einen Fehler in Zeile $1 mit dem Fehlercode $exit_code beendet" exit $exit_code } # Wenn das Skript durch einen Fehler abgebrochen wird, führe error_handler($LINENO) aus trap 'error_handler $LINENO' ERR # Einbinden der Konfigurationsdatei source ./repo_config.sh # Überprüfung der Konstanten aus repo_config.sh source ./check_repo_config.sh make_repo_tree() { local distro_archs for comp in $COMPONENTS; do for distro in "${!DISTRO_ARCH_MAP[@]}"; do distro_archs="${DISTRO_ARCH_MAP[$distro]}" mkdir -p "$REPO_PATH/pool/$comp/$distro" mkdir -p "$REPO_PATH/pool/$comp/all" mkdir -p "$REPO_PATH/pool/$comp/source-all" mkdir -p "$REPO_PATH/pool/$comp/source-bin" mkdir -p "$REPO_PATH/dists/$distro/$comp/binary-all" mkdir -p "$REPO_PATH/dists/$distro/$comp/source" for arch in $distro_archs; do mkdir -p "$REPO_PATH/dists/$distro/$comp/binary-$arch" done done done } make_repo_tree