Pourquoi s'embêter si une archive suffit ?
Une installation en `curl | tar` marche très bien la première fois. Le problème, c'est la deuxième. Rien ne prévient vos utilisateurs qu'une nouvelle version existe, rien ne la met à jour avec leurs autres paquets, et rien ne la désinstalle proprement. Chaque utilisateur finit par inventer son petit rituel de mise à jour.
Un dépôt de paquets confie tout cela à la mécanique que le système d'exploitation possède déjà. `apt-get upgrade` récupère votre nouvelle version avec le reste. La désinstallation, c'est `apt-get remove`. La provenance est une signature GPG que le gestionnaire de paquets vérifie automatiquement, sans que personne ait à y penser.
Le coût, c'est un après-midi de mise en place et un peu de travail par version, que vous pouvez scripter. Voici l'ensemble.
Ce qu'est réellement un dépôt
Les deux formats ne sont que des fichiers statiques servis en HTTPS. Ni démon ni base de données — c'est exactement pour cela qu'un CDN les sert si bien.
Un dépôt APT comporte un `pool/` contenant les fichiers `.deb` et une arborescence `dists/<suite>/` de métadonnées. `Packages` liste chaque paquet avec sa taille, ses hachages et son chemin ; `Release` liste les hachages des fichiers `Packages` ; et la signature couvre `Release`. Voilà toute la chaîne de confiance, et cela comptera plus loin.
Un dépôt YUM est plus plat : les fichiers `.rpm` sont dans un répertoire à côté de `repodata/`, qui contient `repomd.xml` et les index compressés qu'il référence.
Structure du dépôt
deb/
dists/stable/
Release InRelease Release.gpg
main/binary-amd64/Packages{,.gz}
main/binary-arm64/Packages{,.gz}
pool/main/y/yourtool/yourtool_1.0.0_amd64.deb
rpm/
yourtool-1.0.0-1.x86_64.rpm
repodata/
repomd.xml repomd.xml.asc
*-primary.xml.gz *-filelists.xml.gz *-other.xml.gz
Créez une clé de signature
La clé est la seule partie que vous ne pouvez pas refaire à la légère : si vous la perdez, chaque utilisateur doit en importer une nouvelle. Générez-la une fois, sauvegardez à la fois la clé secrète et le certificat de révocation, et gardez la moitié privée hors de toute machine qui n'en a pas besoin.
Les scripts de publication signent sans intervention, donc une clé de signature de dépôt n'a normalement pas de phrase secrète : elle est protégée par les permissions de fichier et par son emplacement restreint, comme une clé de signature de CI. Donnez-lui une vraie expiration (cinq ans est courant) et notez la date de renouvellement là où vous la verrez vraiment.
cat > key.batch <<'EOF'
%no-protection
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign
Name-Real: example.com Package Signing
Name-Email: packages@example.com
Expire-Date: 5y
%commit
EOF
gpg --batch --gen-key key.batch && rm key.batch
# the public half — this is what users import
gpg --armor --export packages@example.com > example-com.gpg
# BACK THESE UP, offline:
# gpg --export-secret-keys --armor packages@example.com
# ~/.gnupg/openpgp-revocs.d/<fingerprint>.rev
Construisez et signez le dépôt APT
`apt-ftparchive` (du paquet `apt-utils`) génère les deux fichiers de métadonnées. Deux détails comptent.
Exécutez-le depuis la racine du dépôt, car les chemins `Filename:` qu'il inscrit dans `Packages` sont relatifs à l'endroit où vous l'invoquez. Si vous vous trompez, apt lira très bien vos métadonnées puis renverra 404 à chaque téléchargement.
Ensuite, publiez la signature deux fois : `InRelease` est la forme moderne signée en ligne, et `Release.gpg` une signature détachée pour les clients plus anciens. Produire les deux ne coûte rien et évite toute une catégorie de questions au support.
cd deb
for arch in amd64 arm64; do
mkdir -p "dists/stable/main/binary-$arch"
apt-ftparchive --arch "$arch" packages pool \
> "dists/stable/main/binary-$arch/Packages"
gzip -9cn "dists/stable/main/binary-$arch/Packages" \
> "dists/stable/main/binary-$arch/Packages.gz"
done
apt-ftparchive \
-o APT::FTPArchive::Release::Origin=example.com \
-o APT::FTPArchive::Release::Suite=stable \
-o APT::FTPArchive::Release::Components=main \
-o APT::FTPArchive::Release::Architectures="amd64 arm64" \
release dists/stable > dists/stable/Release
gpg --batch --yes -abs -o dists/stable/Release.gpg dists/stable/Release
gpg --batch --yes --clearsign -o dists/stable/InRelease dists/stable/Release
RPM exige deux signatures, pas une
C'est l'étape sur laquelle presque tout le monde bute, et nous aussi. APT et YUM vérifient les paquets de manières fondamentalement différentes.
APT est une chaîne : la signature couvre `Release`, qui contient les hachages de `Packages`, qui contient les hachages de chaque `.deb`. Signez les métadonnées et les paquets sont couverts gratuitement — les fichiers `.deb` individuels ne sont pas signés du tout.
RPM effectue deux contrôles indépendants. `repo_gpgcheck=1` vérifie `repomd.xml` via un `repomd.xml.asc` détaché. Mais `gpgcheck=1` vérifie une signature intégrée à l'intérieur de chaque paquet, que rien d'autre ne fournit. Si vous ne signez que les métadonnées, dnf s'arrête avec `Package … is not signed / Error: GPG check FAILED`.
Exécutez donc `rpmsign` sur chaque `.rpm` — et faites-le avant `createrepo_c`, car la signature réécrit les fichiers et tout checksum enregistré auparavant devient faux instantanément.
# rpmsign ships in the 'rpm' package on Debian/Ubuntu.
# rpm hardcodes %__gpg to /usr/bin/gpg2, which Debian/Ubuntu do not ship:
rpmsign --define "_gpg_name packages@example.com" \
--define "__gpg $(command -v gpg)" \
--addsign rpm/*.rpm
# fail loudly rather than publish something unsigned
for r in rpm/*.rpm; do
rpm -qpi "$r" | grep -qi '^Signature *: *(none)' \
&& { echo "UNSIGNED: $r"; exit 1; }
done
# only now build the indexes, then sign repomd.xml
createrepo_c --quiet rpm/
gpg --batch --yes --detach-sign --armor \
-o rpm/repodata/repomd.xml.asc rpm/repodata/repomd.xml
Le servir depuis un CDN — et le piège
Un dépôt de paquets est presque la charge idéale pour un CDN : des fichiers statiques, téléchargés bien plus souvent qu'ils ne changent, par des utilisateurs du monde entier. Pointez le edge vers votre origine et le problème de bande passante disparaît.
Le piège, c'est que les métadonnées d'un dépôt sont cohérentes entre elles. Chaque fichier référence les hachages d'autres fichiers. Un cache qui vous sert un `Release` frais et un `Packages` périmé ne vous a pas donné un dépôt légèrement daté : il vous a donné un dépôt cassé, et le message d'erreur ne mentionnera jamais le cache.
Voici les trois échecs que nous avons rencontrés, lors d'une seule publication :
• `InRelease` périmé avec `Packages` frais : apt signale une non-concordance de hachage.
• `.rpm` périmé : dnf signale `Package … is not signed`, car la signature a réécrit le fichier et la copie en cache est l'ancienne, non signée.
• `repomd.xml` frais avec `repomd.xml.asc` périmé : dnf signale `Bad PGP signature`. Les deux ont changé ; un seul a été purgé.
La règle qui en découle : une purge partielle est pire qu'aucune purge. Purgez tous les fichiers réécrits par la publication — les points d'entrée, les fichiers d'index hachés sous `repodata/`, et les paquets eux-mêmes. Ou donnez aux métadonnées un TTL très court et laissez les paquets en cache longtemps, puisque leurs noms changent à chaque version.
# entry points
for p in /deb/dists/stable/InRelease \
/deb/dists/stable/Release \
/deb/dists/stable/Release.gpg \
/deb/dists/stable/main/binary-amd64/Packages \
/deb/dists/stable/main/binary-amd64/Packages.gz \
/rpm/repodata/repomd.xml \
/rpm/repodata/repomd.xml.asc; do
cdnctl purge --account "$ACC" --path "$p" --type exact
done
# the hashed indexes and the packages changed too
for f in rpm/repodata/* rpm/*.rpm; do
cdnctl purge --account "$ACC" \
--path "/rpm/$(basename "$f")" --type exact
done
Publier : comment les fichiers arrivent sur le CDN
Un dépôt n'est que des fichiers statiques, donc « publier » signifie placer cette arborescence là où le CDN peut la servir. Il y a deux formes, et laquelle vous convient dépend de si vous exploitez déjà un serveur web.
**Si vous avez déjà une origine** — n'importe quelle machine servant du HTTPS — mettez le CDN devant et publiez en y copiant les fichiers. `rsync` via SSH suffit ; le edge récupère depuis l'origine à la première requête puis met en cache. C'est ce que nous faisons pour cdn.com.tr, car la machine de téléchargement est celle qui sert le site.
Un avertissement tiré de l'expérience : synchronisez les sous-répertoires, pas le parent. Un `rsync --delete` visant le répertoire qui contient aussi vos autres téléchargements les supprimera sans hésiter.
**Si vous ne voulez pas exploiter d'origine du tout**, envoyez les fichiers directement dans le stockage du CDN. Avec cdn.com.tr, vous téléversez l'arborescence et elle est servie depuis votre nom de domaine CDN — aucun serveur web à installer, corriger ou maintenir en vie. `cdnctl cp -r` envoie un répertoire entier, ce qui correspond exactement à la forme d'un dépôt.
Dans les deux cas, l'URL de base que vos utilisateurs mettent dans `sources.list` ou le fichier `.repo` est simplement votre domaine CDN plus le chemin de publication. Et dans les deux cas, terminez par l'étape de purge de la section précédente — c'est celle qu'on oublie.
# A) you have an origin: sync the subdirectories, never the parent
rsync -a --delete dist/repo/deb/ user@origin:/var/www/downloads/deb/
rsync -a --delete dist/repo/rpm/ user@origin:/var/www/downloads/rpm/
rsync -a dist/repo/example-com.gpg user@origin:/var/www/downloads/
# B) no origin: push straight into CDN storage
cdnctl cp -r dist/repo/deb <account_uuid>:/downloads/deb
cdnctl cp -r dist/repo/rpm <account_uuid>:/downloads/rpm
cdnctl files put --file dist/repo/example-com.gpg \
--target-path /downloads/example-com.gpg
# the base URL is simply your CDN hostname + that path:
# deb -> https://cdn.example.com/downloads/deb
# rpm -> https://cdn.example.com/downloads/rpm
Vérifiez dans un conteneur vierge
Ne vérifiez pas sur votre propre machine. Elle fait déjà confiance à votre clé, peut détenir un index en cache, et vous cachera volontiers exactement les problèmes que vos utilisateurs vont rencontrer. Un conteneur jetable est le seul test honnête : c'est ainsi que nous avons trouvé à la fois la signature de paquet manquante et les échecs de cache périmé.
Gardez la vérification de signature activée pendant les tests. Désactiver `gpgcheck` pour voir si le reste fonctionne, c'est la façon classique de finir par publier un dépôt non signé.
Un conseil côté Fedora : passez `--disablerepo="*" --enablerepo="votredepot"`. Sinon dnf passe son temps à télécharger les métadonnées de Fedora, et une exécution lente ou échouée ne vous apprend rien sur votre dépôt.
# Debian / Ubuntu
docker run --rm debian:12 sh -c '
apt-get update -qq && apt-get install -y -qq curl gnupg ca-certificates
curl -fsSL https://example.com/example-com.gpg |
gpg --dearmor -o /usr/share/keyrings/example-com.gpg
echo "deb [signed-by=/usr/share/keyrings/example-com.gpg] \
https://example.com/deb stable main" \
> /etc/apt/sources.list.d/example.list
apt-get update && apt-get install -y yourtool && yourtool --version'
# Fedora / RHEL — only your repo, so the test is about your repo
docker run --rm fedora:40 sh -c '
printf "[example]\nbaseurl=https://example.com/rpm\nenabled=1\ngpgcheck=1\nrepo_gpgcheck=1\ngpgkey=https://example.com/example-com.gpg\n" \
> /etc/yum.repos.d/example.repo
dnf --disablerepo="*" --enablerepo="example" -y install yourtool
rpm -qi yourtool | grep Signature'
Un dernier point : désactivez l'auto-mise à jour dans les paquets
Si votre outil sait se mettre à jour lui-même, cette fonction entre désormais en conflit avec le gestionnaire de paquets. Une auto-mise à jour écrase un fichier appartenant à `dpkg` ou `rpm`, et leur base de données continue de pointer vers une version qui n'est plus sur le disque — la prochaine mise à jour fait alors quelque chose de surprenant.
Le correctif est minime : marquez la compilation avec son mode de distribution et faites en sorte que l'auto-updater s'efface quand un gestionnaire de paquets l'a installé. Les téléchargements directs continuent de se mettre à jour comme avant.
// installChannel is overridden at build time for packaged builds.
var installChannel = "direct"
func update() error {
if installChannel != "direct" {
return fmt.Errorf(
"installed via %s — upgrade it with that instead",
installChannel)
}
// ... normal self-update
}
// packaged builds:
// go build -ldflags "-X main.installChannel=deb"
Questions fréquentes
Suis-je obligé de signer le dépôt ?
Techniquement non : on peut demander à apt et dnf de sauter la vérification. En pratique oui. Vos utilisateurs devraient désactiver un contrôle de sécurité pour installer votre logiciel, ce qui est déjà pénible à demander et pire encore à enseigner. Signer coûte une clé et deux commandes.
Pourquoi dnf dit-il que mon paquet n'est pas signé alors que j'ai signé les métadonnées ?
Parce que RPM vérifie deux choses distinctes. `repo_gpgcheck` couvre `repomd.xml` ; `gpgcheck` couvre une signature intégrée à chaque `.rpm`. Il faut aussi `rpmsign --addsign` sur les paquets — et cela doit tourner avant `createrepo_c`, puisque la signature modifie les fichiers.
Pourquoi apt signale-t-il une non-concordance de hachage juste après la publication ?
Presque toujours une couche de cache servant un mélange d'anciennes et de nouvelles métadonnées. Les métadonnées d'un dépôt sont cohérentes entre elles, donc une mise à jour partielle est un dépôt cassé. Purgez chaque fichier réécrit par la publication, ou donnez un TTL court aux métadonnées.
Un seul dépôt peut-il servir amd64 et arm64 ?
Oui. Pour APT, générez un fichier `Packages` par architecture sous la même suite et listez les deux dans `Architectures`. Pour YUM, placez les deux paquets dans le même répertoire — `createrepo_c` enregistre l'architecture de chacun.
Les fichiers du dépôt doivent-ils être dans git ?
Généralement non. Les pools de paquets grossissent à chaque version et git conserve tout indéfiniment. Construisez l'arborescence à partir de vos artefacts de publication et synchronisez-la vers l'origine, en ne gardant dans git que les scripts et la clé publique. Rendez la construction reproductible pour pouvoir recréer le dépôt de zéro à tout moment.