Reverse Proxy & TLS¶
Configurer un reverse proxy avec terminaison TLS devant Repod pour activer HTTPS, exposer les services sur des ports standards, et durcir la surface d'attaque publique.
Pourquoi un reverse proxy ?¶
Repod expose trois services en HTTP brut par défaut :
| Service | Port par défaut | Description |
|---|---|---|
| Interface web | 3003 |
React SPA |
| Backend API | 8000 |
FastAPI — appelé directement par les navigateurs |
| Dépôt | 80 |
Servi par Nginx interne |
Sans reverse proxy :
- Les identifiants et tokens API circulent en clair sur le réseau.
- Les navigateurs modernes signalent les sites en HTTP simple.
- HSTS ne peut pas être activé.
- Les clients APT/DNF sont vulnérables aux attaques de type interception (man-in-the-middle).
Avec un reverse proxy TLS, vous obtenez :
- Un chiffrement de bout en bout (TLS 1.2/1.3).
- HSTS pour imposer HTTPS dans les navigateurs.
- Les ports standards 80/443 — aucun suffixe de port dans les URLs.
- Une gestion centralisée des certificats (Let's Encrypt ou CA interne).
Architecture cible¶
┌───────────────────────────────────────┐
│ Reverse Proxy │
Internet ─ :443 ─► │ / → frontend-ui :3003 │
│ /api/* → backend-api :8000 │
─ :80 ─► │ redirect to HTTPS │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ Repository (HTTP OK) │
LAN clients ─ :80 ─► depot-apt / depot-rpm :80 │
└───────────────────────────────────────┘
Le dépôt en HTTP
Le dépôt APT/RPM peut rester en HTTP simple — les paquets sont signés par GPG. APT et DNF vérifient la signature ; l'intégrité en HTTP est gérée au niveau du paquet. Ne migrez vers HTTPS que si votre politique de sécurité l'exige.
Avant de commencer¶
Définissez BIND_HOST=127.0.0.1 dans votre fichier .env pour que les
services n'écoutent que sur l'interface loopback. Le reverse proxy gère
alors toute l'exposition externe :
Reconstruisez et redémarrez après avoir modifié .env :
Option A — Nginx + Let's Encrypt (recommandé)¶
Installer Nginx et Certbot¶
Obtenir le certificat TLS¶
Certbot modifie la configuration Nginx et configure le renouvellement automatique.
Configuration Nginx complète¶
Créez /etc/nginx/sites-available/repod (Debian/Ubuntu) ou
/etc/nginx/conf.d/repod.conf (RHEL/AlmaLinux) :
# Redirection HTTP → HTTPS
server {
listen 80;
server_name repo.example.com;
return 301 https://$host$request_uri;
}
# Frontend + API en HTTPS
server {
listen 443 ssl http2;
server_name repo.example.com;
ssl_certificate /etc/letsencrypt/live/repo.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/repo.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# HSTS — n'activer qu'après avoir vérifié que HTTPS fonctionne de bout en bout
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Frontend (React SPA)
location / {
proxy_pass http://127.0.0.1:3003;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API — retirer le préfixe /api avant de transmettre
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s; # laisser le temps aux scans CVE
client_max_body_size 512m; # autoriser les uploads de gros paquets
# Requis pour les Server-Sent Events (progression sync / import)
proxy_buffering off;
proxy_cache off;
}
}
Activer et recharger :
# Debian / Ubuntu
ln -s /etc/nginx/sites-available/repod /etc/nginx/sites-enabled/repod
# Toutes plateformes
nginx -t
sudo systemctl reload nginx
Variante — domaine unique avec préfixe de chemin /api/¶
Si vous ne voulez pas mettre le frontend et l'API sur des ports séparés, le
bloc location /api/ ci-dessus route correctement les appels API. Mettez à
jour REACT_APP_API_URL en conséquence et reconstruisez le frontend :
Option B — Nginx + CA interne / certificat auto-signé¶
Utile pour les intranets ou les environnements air-gapped sans accès à Internet public.
Générer un certificat auto-signé¶
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/repod.key \
-out /etc/ssl/certs/repod.crt \
-subj "/CN=repo.example.com" \
-addext "subjectAltName=DNS:repo.example.com,IP:192.168.1.100"
Remplacez les lignes ssl_certificate* dans la configuration Nginx ci-dessus :
Distribuer le certificat aux clients¶
Option C — Traefik (natif Docker)¶
Repod prend en charge Traefik dans trois configurations, selon qui possède l'instance Traefik : intégrée à Repod, auto-gérée avec des labels Docker, ou un Traefik existant que vous ne contrôlez pas.
C1 — Overlay intégré de Repod (recommandé)¶
Repod fournit un overlay prêt à l'emploi, docker-compose.traefik.yml, ainsi
que traefik/traefik.yml et traefik/dynamic.yml. Il est mutuellement
exclusif avec docker-compose.tls.yml (même rôle — choisir l'un ou l'autre,
jamais les deux) et reproduit le même routage (frontend /, API directe sur
:8443) que l'overlay TLS Nginx.
bash scripts/gen-selfsigned-certs.sh
docker compose -f docker-compose.yaml -f docker-compose.traefik.yml up -d
Pourquoi le file provider, pas les labels Docker
Cet overlay utilise délibérément le file provider de Traefik
(traefik/dynamic.yml) plutôt que son Docker provider. Le Docker
provider nécessite de monter /var/run/docker.sock dans le conteneur
du proxy — un conteneur avec accès au socket peut contrôler tous les
conteneurs de l'hôte, y compris les données d'autres tenants en mode
SaaS. Repod évite de monter le socket Docker partout ailleurs (voir les
sections HA et registre OCI de la documentation d'architecture), donc
l'overlay intégré suit la même règle. Le compromis : ajouter une
nouvelle route implique d'éditer traefik/dynamic.yml (rechargé à
chaud, aucun redémarrage nécessaire) plutôt que d'ajouter un label — le
même effort que de maintenir un vhost Nginx.
Let's Encrypt est pris en charge nativement (voir le bloc commenté dans
traefik/traefik.yml) — Traefik renouvelle les certificats lui-même, aucun
conteneur Certbot séparé n'est requis.
C2 — Traefik auto-géré avec labels Docker¶
Si vous préférez utiliser la découverte automatique Docker de Traefik plutôt que l'overlay intégré basé sur le file provider, voici la configuration équivalente. Celle-ci nécessite réellement un accès au socket Docker pour le conteneur Traefik — acceptable pour un hôte mono-tenant à usage unique ; à peser plus soigneusement pour un déploiement multi-tenant/SaaS (voir l'encart ci-dessus).
services:
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.tlschallenge=true"
- "--certificatesresolvers.le.acme.email=admin@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
restart: unless-stopped
frontend-ui:
labels:
- "traefik.enable=true"
- "traefik.http.routers.repod-ui.rule=Host(`repo.example.com`)"
- "traefik.http.routers.repod-ui.entrypoints=websecure"
- "traefik.http.routers.repod-ui.tls.certresolver=le"
- "traefik.http.services.repod-ui.loadbalancer.server.port=3003"
backend-api:
labels:
- "traefik.enable=true"
- "traefik.http.routers.repod-api.rule=Host(`repo.example.com`) && PathPrefix(`/api/`)"
- "traefik.http.routers.repod-api.entrypoints=websecure"
- "traefik.http.routers.repod-api.tls.certresolver=le"
- "traefik.http.routers.repod-api.middlewares=strip-api"
- "traefik.http.middlewares.strip-api.stripprefix.prefixes=/api"
- "traefik.http.services.repod-api.loadbalancer.server.port=8000"
| Avantage | Inconvénient |
|---|---|
| Aucune gestion manuelle de certificat | Nécessite un accès au socket Docker |
| Renouvellement automatique | Syntaxe de labels verbeuse |
| Découverte automatique des services | Conteneur supplémentaire à gérer |
C3 — Intégration avec un Traefik que vous ne gérez pas¶
Cas fréquent lors d'un déploiement sur l'infrastructure d'un client : il fait déjà tourner Traefik (ailleurs sur le même hôte Docker, sur un hôte séparé, ou en tant qu'ingress Kubernetes) et veut que Repod se place derrière plutôt que d'exécuter son propre proxy.
Ne déployez aucun des overlays ci-dessus (docker-compose.traefik.yml
ou les labels de C2) — ils entreraient en conflit avec l'instance existante.
Procédez plutôt ainsi :
-
Démarrez Repod avec uniquement le fichier compose de base — aucun overlay TLS/proxy :
-
Restreignez
frontend-ui/backend-apiau segment réseau que leur Traefik peut atteindre (BIND_HOSTdans.env, ou segmentation réseau/pare-feu) — le frontend transmet déjà/api/au backend en interne, donc seul un upstream (:3003) doit être accessible depuis leur proxy. -
Communiquez à leur équipe la route à ajouter de leur côté. S'ils utilisent le file provider de Traefik, voici l'extrait complet :
dynamic.yml (leur Traefik)http: routers: repod: rule: "Host(`repod.customer.com`)" entryPoints: ["websecure"] service: repod tls: {} services: repod: loadBalancer: servers: - url: "http://<repod-host>:3003"S'ils utilisent des labels Docker à la place, et que leur Traefik peut atteindre le réseau Docker de Repod, l'équivalent est un unique bloc de labels sur
frontend-ui(même forme que les labelsfrontend-uide C2, pointés vers leur proprecertresolver/rule). -
Transmettez-leur ces trois exigences — elles sont faciles à manquer et chacune casse quelque chose de différent :
Exigence Pourquoi CORS_ORIGINS=https://repod.customer.comdansbackend.envSans cela, les appels API du frontend lui-même sont rejetés par le navigateur Transmettre l'en-tête HostTraefik le fait par défaut (contrairement à Nginx, qui nécessite un proxy_set_header Host $hostexplicite) — important pour la résolution de tenant SaaS, qui lit cet en-têteAucune limite de taille de corps de requête sur la route Repod Les uploads .deb/.rpmpeuvent être volumineux (jusqu'à 512 Mo) ; Traefik n'a pas de limite par défaut, mais un middleware qu'ils exécutent déjà ailleurs pourrait en imposer uneAucun changement n'est nécessaire côté Nginx interne de Repod (
depot-apt/depot-rpm) — ces conteneurs continuent de gérer leurs propres vérifications d'accès aux distributions basées surauth_request, quel que soit le reverse proxy placé devant l'ensemble de la stack.
Option D — Caddy (configuration minimale)¶
Caddy gère HTTPS automatiquement avec une configuration minimale.
repo.example.com {
# Frontend
reverse_proxy / http://127.0.0.1:3003
# Backend API — retirer le préfixe /api
handle /api/* {
uri strip_prefix /api
reverse_proxy http://127.0.0.1:8000 {
header_up X-Forwarded-Proto {scheme}
transport http { read_timeout 300s }
}
}
request_body { max_size 512MB }
}
Caddy obtient et renouvelle automatiquement les certificats Let's Encrypt.
Mettre à jour la configuration Repod après l'activation de HTTPS¶
Après avoir activé le reverse proxy, mettez à jour les variables d'environnement et reconstruisez le frontend.
.env¶
BIND_HOST=127.0.0.1
REACT_APP_API_URL=https://repo.example.com/api
REACT_APP_REPO_URL=http://repo.example.com
backend.env¶
Pour Traefik à l'intérieur de Docker (réseau bridge), incluez aussi le sous-réseau Docker :
Reconstruire le frontend¶
Les variables REACT_APP_* sont intégrées au bundle JavaScript au moment de la construction :
Vérifier la configuration¶
# Chaîne de certificat
curl -vI https://repo.example.com 2>&1 | grep -E "SSL|TLS|certificate|issuer"
# En-tête HSTS
curl -sI https://repo.example.com | grep -i strict-transport
# Redirection HTTP → HTTPS
curl -I http://repo.example.com
# Attendu : 301 Moved Permanently → https://...
# API en HTTPS
curl -s https://repo.example.com/api/health/live | jq .
Renouvellement des certificats¶
| Proxy | Renouvellement | Action requise |
|---|---|---|
| Certbot | Timer systemd toutes les 12h | Aucune — s'exécute automatiquement |
| Traefik | Client ACME intégré | Aucune |
| Caddy | Client ACME intégré | Aucune |
| Auto-signé | Manuel ou cron | Planifier un renouvellement annuel |
Vérifier l'état du timer Certbot :
systemctl status certbot.timer
certbot renew --dry-run # tester le renouvellement sans toucher aux fichiers
Checklist post-configuration¶
| Action | Fichier / Commande |
|---|---|
BIND_HOST=127.0.0.1 |
.env |
REACT_APP_API_URL mis à jour |
.env → reconstruire le frontend |
CORS_ORIGINS mis à jour |
backend.env |
TRUSTED_PROXIES défini |
backend.env |
| Frontend reconstruit | docker compose build frontend-ui |
| En-tête HSTS vérifié | curl -sI https://repo.example.com |
| Port 8000 non exposé | sudo ufw status ou firewall-cmd --list-all |