Aller au contenu

Reverse proxy

Un reverse proxy placé devant les deux processus termine le TLS et les publie sous des noms publics. Cette page indique ce qu'il doit faire, puis donne une configuration pour nginx, Apache, HAProxy et Traefik.

Ce que doit faire le proxy

Router chaque chemin vers son processus. Le serveur MCP et le serveur Controller sont deux processus (voir Architecture) :

Chemins Processus Port (fichiers Compose)
/mcp, /.well-known/oauth-protected-resource/… (connexion OAuth) serveur MCP 8000
tout le reste : /ui, /api, /.well-known/oauth-authorization-server, /authorize, /token, /register, /revoke serveur Controller 8080

Avec un hôte par processus (mcp.secreview.example.com et secreview.example.com), chaque hôte envoie tous ses chemins à son processus : aucune règle de chemin. Avec un seul hôte, routez par chemin comme dans les exemples ci-dessous.

Déclarer les URL publiques. Les serveurs donnent aux clients des URL absolues construites à partir des réglages, jamais de l'en-tête Host : ce doivent être celles du proxy, telles que les postes les voient.

Variable (serveur Controller) Un hôte par processus Un seul hôte
SECREVIEW_PUBLIC_URL https://secreview.example.com https://secreview.example.com
SECREVIEW_MCP_URL (connexion OAuth) https://mcp.secreview.example.com/mcp https://secreview.example.com/mcp

Les hôtes utilisent alors la même adresse que SECREVIEW_MCP_URL.

Garder les réponses MCP en flux. /mcp peut répondre par des server-sent events : aucune mise en mémoire tampon des réponses, et un délai de lecture assez long pour une revue (une heure ci-dessous).

Laisser passer les corps. Un scan de security-review-ci envoie jusqu'à SECREVIEW_MAX_SCAN_REQUEST_MB (64 Mio) au serveur Controller, une requête MCP jusqu'à SECREVIEW_MAX_MCP_REQUEST_MB (16 Mio) : la limite du proxy ne doit pas être plus basse.

Transmettre l'adresse du client. Le proxy envoie X-Forwarded-For et X-Forwarded-Proto ; les deux processus ne les croient que depuis les adresses de SECREVIEW_FORWARDED_ALLOW_IPS (limitation des connexions et des requêtes, journal d'audit, HSTS). Avec les fichiers Compose et un proxy sur l'hôte, les conteneurs voient l'adresse du pont Docker, pas 127.0.0.1 : réglez par exemple SECREVIEW_FORWARDED_ALLOW_IPS=172.16.0.0/12 (la plage de vos réseaux Docker).

Les exemples utilisent un seul hôte secreview.example.com, le TLS terminé par le proxy et les ports des fichiers Compose sur 127.0.0.1. Pour un hôte par processus, gardez la partie serveur Controller sur secreview.example.com et donnez à la partie MCP son propre hôte, sans règle de chemin. Quand le proxy tourne sur une autre machine que les processus, les jetons traverseraient le réseau en clair : activez le TLS intégré des deux processus et pointez le proxy vers https://… au lieu de http://127.0.0.1.

nginx

upstream secreview_controller { server 127.0.0.1:8080; }
upstream secreview_mcp { server 127.0.0.1:8000; }

server {
    listen 443 ssl;
    http2 on;
    server_name secreview.example.com;
    ssl_certificate     /etc/ssl/secreview/fullchain.pem;
    ssl_certificate_key /etc/ssl/secreview/privkey.pem;

    client_max_body_size 64m;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # Serveur MCP : le point d'accès (réponses en flux) et ses métadonnées OAuth
    location /mcp {
        proxy_pass http://secreview_mcp;
        proxy_buffering off;
        proxy_read_timeout 1h;
    }
    location /.well-known/oauth-protected-resource {
        proxy_pass http://secreview_mcp;
    }

    # Serveur Controller : interface web, API REST, serveur d'autorisation OAuth
    location / {
        proxy_pass http://secreview_controller;
    }
}

Apache

Modules proxy, proxy_http, ssl et headers (a2enmod proxy proxy_http ssl headers). mod_proxy ajoute lui-même X-Forwarded-For.

<VirtualHost *:443>
    ServerName secreview.example.com
    SSLEngine on
    SSLCertificateFile    /etc/ssl/secreview/fullchain.pem
    SSLCertificateKeyFile /etc/ssl/secreview/privkey.pem

    ProxyPreserveHost On
    ProxyTimeout 3600
    LimitRequestBody 67108864
    RequestHeader set X-Forwarded-Proto "https"
    # Pas de compression des réponses MCP en flux
    SetEnvIf Request_URI "^/mcp" no-gzip

    # Serveur MCP d'abord (les chemins les plus précis ne l'emportent que déclarés en premier)
    ProxyPass        /mcp http://127.0.0.1:8000/mcp flushpackets=on
    ProxyPassReverse /mcp http://127.0.0.1:8000/mcp
    ProxyPass        /.well-known/oauth-protected-resource http://127.0.0.1:8000/.well-known/oauth-protected-resource

    # Serveur Controller : tout le reste
    ProxyPass        / http://127.0.0.1:8080/
    ProxyPassReverse / http://127.0.0.1:8080/
</VirtualHost>

HAProxy

frontend secreview
    mode http
    bind :443 ssl crt /etc/haproxy/certs/secreview.pem alpn h2,http/1.1
    option forwardfor
    http-request set-header X-Forwarded-Proto https
    timeout client 1h

    acl to_mcp path /mcp
    acl to_mcp path_beg /mcp/ /.well-known/oauth-protected-resource
    use_backend secreview_mcp if to_mcp
    default_backend secreview_controller

backend secreview_mcp
    mode http
    option httpchk GET /healthz
    timeout server 1h
    server mcp 127.0.0.1:8000 check

backend secreview_controller
    mode http
    option httpchk GET /healthz
    timeout server 5m
    server controller 127.0.0.1:8080 check

HAProxy ne met pas les réponses en mémoire tampon et ne fixe aucune limite de corps par défaut.

Traefik

Configuration dynamique (fournisseur de fichiers). Traefik envoie X-Forwarded-*, transmet les server-sent events au fil de l'eau et donne la priorité à la règle la plus longue : le routeur MCP l'emporte sur ses chemins.

http:
  routers:
    secreview-mcp:
      rule: Host(`secreview.example.com`) && (PathPrefix(`/mcp`) || PathPrefix(`/.well-known/oauth-protected-resource`))
      entryPoints: [websecure]
      service: secreview-mcp
      tls:
        certResolver: letsencrypt
    secreview-controller:
      rule: Host(`secreview.example.com`)
      entryPoints: [websecure]
      service: secreview-controller
      tls:
        certResolver: letsencrypt
  services:
    secreview-mcp:
      loadBalancer:
        servers:
          - url: http://127.0.0.1:8000
    secreview-controller:
      loadBalancer:
        servers:
          - url: http://127.0.0.1:8080

Avec le fournisseur Docker, les mêmes routeurs vont dans les labels des services mcp et controller (traefik.http.routers.secreview-mcp.rule=…), chacun avec traefik.http.services.<nom>.loadbalancer.server.port=8000, le port dans le conteneur.

Vérifier

curl -s https://secreview.example.com/.well-known/oauth-protected-resource/mcp   # serveur MCP (OAuth actif)
curl -s https://secreview.example.com/.well-known/oauth-authorization-server     # serveur Controller (OAuth actif)
curl -si -X POST https://secreview.example.com/mcp | head -1                     # 401 (OAuth actif)
curl -s https://secreview.example.com/api/health                                 # {"status":"ok"}

L'issuer de la deuxième réponse doit être exactement SECREVIEW_PUBLIC_URL, et les authorization_servers de la première la même URL.