Aller au contenu

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 :

.env
BIND_HOST=127.0.0.1

Reconstruisez et redémarrez après avoir modifié .env :

docker compose up -d

Option A — Nginx + Let's Encrypt (recommandé)

Installer Nginx et Certbot

sudo apt install -y nginx certbot python3-certbot-nginx
sudo dnf install -y nginx certbot python3-certbot-nginx
sudo systemctl enable --now nginx

Obtenir le certificat TLS

certbot --nginx -d repo.example.com

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) :

/etc/nginx/sites-available/repod
# 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 :

.env
REACT_APP_API_URL=https://repo.example.com/api
docker compose build frontend-ui
docker compose up -d frontend-ui

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 :

ssl_certificate     /etc/ssl/certs/repod.crt;
ssl_certificate_key /etc/ssl/private/repod.key;

Distribuer le certificat aux clients

# Sur chaque machine cliente
sudo cp repod.crt /usr/local/share/ca-certificates/repod.crt
sudo update-ca-certificates

# Spécifiquement pour apt
echo 'Acquire::https::repo.example.com::CaInfo "/etc/ssl/certs/repod.crt";' \
  | sudo tee /etc/apt/apt.conf.d/99repod-tls
# Sur chaque machine cliente
sudo cp repod.crt /etc/pki/ca-trust/source/anchors/repod.crt
sudo update-ca-trust extract

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).

docker-compose.yaml (ajouts)
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"
      - "[email protected]"
      - "--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
docker-compose.yaml (labels frontend-ui et backend-api)
  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 :

  1. Démarrez Repod avec uniquement le fichier compose de base — aucun overlay TLS/proxy :

    docker compose -f docker-compose.yaml up -d
    
  2. Restreignez frontend-ui/backend-api au segment réseau que leur Traefik peut atteindre (BIND_HOST dans .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.

  3. 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 labels frontend-ui de C2, pointés vers leur propre certresolver/rule).

  4. Transmettez-leur ces trois exigences — elles sont faciles à manquer et chacune casse quelque chose de différent :

    Exigence Pourquoi
    CORS_ORIGINS=https://repod.customer.com dans backend.env Sans cela, les appels API du frontend lui-même sont rejetés par le navigateur
    Transmettre l'en-tête Host Traefik le fait par défaut (contrairement à Nginx, qui nécessite un proxy_set_header Host $host explicite) — important pour la résolution de tenant SaaS, qui lit cet en-tête
    Aucune limite de taille de corps de requête sur la route Repod Les uploads .deb/.rpm peuvent ê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 une

    Aucun 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 sur auth_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.

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install caddy
dnf install -y 'dnf-command(copr)'
dnf copr enable @caddy/caddy
dnf install caddy
/etc/caddy/Caddyfile
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 }
}
sudo systemctl enable --now caddy
caddy reload --config /etc/caddy/Caddyfile

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

.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

backend.env
CORS_ORIGINS=https://repo.example.com
TRUSTED_PROXIES=127.0.0.1

Pour Traefik à l'intérieur de Docker (réseau bridge), incluez aussi le sous-réseau Docker :

TRUSTED_PROXIES=127.0.0.1,172.16.0.0/12

Reconstruire le frontend

Les variables REACT_APP_* sont intégrées au bundle JavaScript au moment de la construction :

docker compose build frontend-ui
docker compose up -d frontend-ui

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