Skip to content

feat(tls) : un certificat unique pour l'UI, l'API et les dépôts - #5

Open
TiotBenjy wants to merge 6 commits into
repod-ce:mainfrom
TiotBenjy:feat/secure-repos
Open

TiotBenjy wants to merge 6 commits into
repod-ce:mainfrom
TiotBenjy:feat/secure-repos

Conversation

@TiotBenjy

Copy link
Copy Markdown
Contributor

Problème

Fournir un certificat à Repod demandait de connaître trois procédures distinctes, dont deux n'étaient écrites nulle part.

  • Auto-signé : scripts/gen-selfsigned-certs.sh, seul chemin documenté.
  • Let's Encrypt : après émission, il fallait éditer à la main
    nginx/tls-proxy.conf pour le faire pointer vers /certs/letsencrypt/live/$DOMAIN/. C'est un fichier suivi par git : la modification entre en conflit au prochain git pull et n'est rejouée sur aucune nouvelle instance. La première émission butait en plus sur une dépendance circulaire non documentée : nginx refuse de démarrer sans certificat, et sans nginx le challenge ACME n'est pas servi sur :80. Le renouvellement n'était câblé nulle part.
  • Autorité interne (easy-rsa, AD CS) : le cas courant d'un dépôt privé, fonctionnel par convention mais absent de toute documentation, pièges compris (chaîne incomplète, clé chiffrée, PFX à convertir).

Et le certificat obtenu ne couvrait que l'UI et l'API : les dépôts de paquets restaient en HTTP clair alors que le proxy TLS était déjà devant eux. La signature GPG protège l'intégrité, mais l'inventaire des paquets et des versions installées circulait en clair. En mode TLS, la page « Configuration client » générait par-dessus le marché des commandes bâties sur https://<host>:80, une URL qui ne répond pas.

Ce que fait cette PR

Un point d'entrée unique, scripts/setup-tls.sh, qui alimente toujours le même couple canonique repos/certs/tls/{cert.pem,key.pem} (celui que nginx et Traefik lisent déjà). Plus aucun fichier suivi par git à éditer après une émission, et le script recharge lui-même le proxy actif.

setup-tls.sh self-signed [--san dns:a --san ip:1.2.3.4 ...] [HÔTE]
setup-tls.sh csr --san dns:a --san ip:1.2.3.4 [--cn NOM] [--key-type TYPE]
setup-tls.sh import --cert F [--key F] [--chain F] | --pkcs12 F
setup-tls.sh letsencrypt --domain D --email E [--staging]
setup-tls.sh renew
setup-tls.sh status

Le mode csr produit une requête multi-SAN à faire signer par l'autorité de l'organisation : un dépôt interne est joint sous plusieurs noms, et souvent aussi par IP depuis les machines sans résolution DNS. La clé est écrite dans pending/, jamais au chemin canonique (il peut s'écouler des jours avant la signature), pendant lesquels le certificat en service ne doit pas bouger. Au retour, import --cert repod.crt --chain ca.pem sans --key reprend cette clé, vérifie qu'elle correspond, installe et vide pending/.

Tous les contrôles tournent avant la moindre écriture, et l'installation est atomique. Sont refusés : un certificat qui ne correspond pas à la clé, une clé protégée par une phrase de passe (nginx démarre sans terminal, elle ne pourrait
jamais être saisie), un certificat expiré. Sont signalés : une chaîne intermédiaire absente ou dans le mauvais ordre (l'échec habituel d'une PKI AD CS à deux niveaux), qu'il vaut mieux voir avant la mise en service que dans les journaux des clients (et une expiration à moins de trente jours).

Côté Let's Encrypt, un --deploy-hook recopie le certificat vers le chemin canonique. Le hook étant enregistré dans le fichier de renouvellement à l'émission, certbot renew le rejoue seul. Un mode résident sans cron est fourni en bloc commenté, sans jamais monter le socket Docker.

Enfin, les dépôts sont servis par le proxy sur :443 sous le même certificat que l'UI : /repos/ et /apk/ vers apt-repo, /rpm/ vers rpm-repo, avec parité Traefik.

Découpage

Commit Objet
feat(tls): point d'entrée unique de fourniture du certificat scripts/setup-tls.sh
feat(tls): installer le certificat Let's Encrypt sans éditer la conf nginx deploy-hook, montage, mode résident
fix(stats): préserver l'IP client des téléchargements derrière un proxy real_ip dans les deux dépôts
feat(tls): servir les dépôts APT/RPM/APK derrière le certificat du proxy nginx et Traefik
fix(ui): URLs de dépôt cassées en mode TLS sur la page Configuration client api.js + 8 tests
docs(tls): réécrire la section de déploiement TLS README, .env.example

Le commit real_ip précède volontairement l'exposition des dépôts : download_stats.py compte les clients uniques sur $remote_addr, et sans lui tous les téléchargements passant par le proxy se confondraient en un seul client. Isolé, il est sans effet.

Compatibilité

Rien ne casse, et rien n'est à migrer côté clients.

  • Les ports directs restent publiés : les sources.list et .repo déjà déployés continuent de fonctionner à l'identique. Le HTTPS est un accès supplémentaire, pas un remplacement.
  • scripts/gen-selfsigned-certs.sh n'est pas modifié, et setup-tls.sh self-signed <HÔTE> lui délègue : résultat identique à
    l'existant dans le cas mono-hôte.
  • Le montage de repos/certs/letsencrypt dans nginx-proxy est conservé : les déploiements qui ont déjà fait pointer tls-proxy.conf vers live/$DOMAIN/ continuent de fonctionner.
  • Aucun fichier backend n'est touché.

Vérifications

  • Pile de test nginx complète (apt-repo, rpm-repo, frontend, proxy, client) : /repos/dists/…, /repos/pool/*.deb, /apk/…/APKINDEX.tar.gz, /rpm/…/repodata/repomd.xml et /rpm/…/*.rpm répondent 200 en HTTPS avec le bon contenu, / atteint toujours le frontend, et les en-têtes de sécurité du bloc server sont bien hérités par les location de dépôt.
  • Redirections : /rpm => /rpm/, /rpm/almalinux9 => /rpm/almalinux9/, /repos/pool → /repos/pool/, toutes en https:// et préfixe conservé.
  • downloads.log journalise l'IP du conteneur client et non celle du proxy, pour un .deb comme pour un .rpm.
  • Même pile sous Traefik : UI et trois familles de dépôts servies, StripPrefix correct, HSTS présent, IP client préservée.
  • Cycle complet de la requête : csr multi-SAN, signature par une PKI de test à deux niveaux, import sans --key. Refus vérifiés sur clé non correspondante, clé chiffrée, mauvais mot de passe PKCS#12, requête déjà en attente, csr sans --san.
  • Import d'un PKCS#12 fabriqué avec openssl pkcs12 -export : feuille et intermédiaire installés.
  • nginx -t vert sur les trois configurations, docker compose config vert sur les trois combinaisons d'overlays, shellcheck propre, vitest run à 58 tests verts dont 8 nouveaux.

Pour rejouer localement

bash scripts/setup-tls.sh self-signed --san dns:repod.local --san ip:127.0.0.1
docker compose -f docker-compose.yaml -f docker-compose.tls.yml up -d
curl -k https://localhost/repos/dists/<codename>/Release
curl -k https://localhost/rpm/<distribution>/x86_64/repodata/repomd.xml
bash scripts/setup-tls.sh status

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant