---
title: "Proxy web frontal"
weight: 25
description: "Exposer l'interface web et l'API derrière Apache ou Nginx : terminaison TLS, en-têtes proxy, fichiers de configuration complets."
---

# Proxy web frontal (Apache ou Nginx)

`serve-web` écoute uniquement en loopback (`web.bind: "127.0.0.1:8080"` par
défaut) et sert tout sur ce port : l'interface web, les pages publiques
(`/p/...`), la page d'aide d'une liste (`/l/<id>/help`, annoncée par
`List-Help`), `robots.txt`, l'API REST (`/api/v1/...`) et les sondes
(`/healthz`, `/readyz`).
Il ne s'expose jamais directement : un proxy frontal termine le TLS et relaie
les requêtes vers le loopback.

Le TLS au frontal est obligatoire, pas décoratif : `serve-web` marque ses
cookies `Secure` (sauf `--insecure`, réservé au développement local). Servie en
HTTP nu, l'interface serait inutilisable : le navigateur ne renverrait jamais
le cookie de session. difuzio envoie lui-même `Strict-Transport-Security`
(HSTS) et ses en-têtes de sécurité (CSP, `X-Frame-Options`, ...) : inutile de
les dupliquer au frontal.

Le vhost porte le nom du `base_url` du domaine : les liens contenus dans les
mails (confirmation, désabonnement) pointent dessus. Avec plusieurs domaines,
dupliquer le vhost pour chaque `base_url` ; tous relaient vers le même backend.

## Deux façons de servir l'interface

difuzio propose deux interfaces, qui se déploient différemment :

- L'**interface rendue par le serveur**, historique, servie directement par
  `serve-web`. Elle ne demande aucun fichier statique : les vhosts ci-dessous
  suffisent tels quels.
- La **console d'administration** (SPA), construite depuis `webui/`. C'est un
  jeu de fichiers statiques à déployer, l'API restant proxifiée vers le
  backend. Le fait de servir les deux sur la MÊME origine est ce qui permet au
  cookie de session de fonctionner sans CORS.

Pour la console, construire puis déployer le résultat :

    make webui-install     # une seule fois, ou après une mise à jour des dépendances
    make webui-build       # produit webui/dist/
    rsync -a --delete webui/dist/ root@serveur:/var/www/difuzio-console/

Le vhost doit alors router **dans cet ordre** : les préfixes du backend
(`/api/`, `/p/`, `/l/`, `/robots.txt`, `/healthz`, `/readyz`) vers `serve-web`,
puis tout le reste vers les fichiers statiques, avec un repli sur `index.html`
pour les routes internes de la console. L'ordre compte : `/p/` et `/l/` servent
les liens contenus dans les mails (désabonnement, abonnement, archives, page
d'aide annoncée par `List-Help`), ils ne doivent jamais être avalés par le repli
de la SPA.

Les sections Nginx et Apache ci-dessous donnent d'abord la configuration
minimale (interface serveur seule), puis le bloc à ajouter pour la console.

## Nginx

`/etc/nginx/sites-available/difuzio.conf` :

    # difuzio - vhost frontal ; un bloc server par base_url de domaine.
    server {
        listen 80;
        listen [::]:80;
        server_name lists.example.org;
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name lists.example.org;

        ssl_certificate     /etc/letsencrypt/live/lists.example.org/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/lists.example.org/privkey.pem;

        # L'API limite ses corps JSON à 1 MiB ; 2m laisse de la marge aux formulaires.
        client_max_body_size 2m;

        access_log /var/log/nginx/difuzio.access.log;

        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host              $host;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto https;
            proxy_set_header X-Forwarded-Host  $host;
            # web.write_timeout (60 s par défaut) doit rester sous ces valeurs.
            proxy_read_timeout 90s;
            proxy_send_timeout 90s;
        }
    }

Activation :

    ln -s /etc/nginx/sites-available/difuzio.conf /etc/nginx/sites-enabled/
    nginx -t && systemctl reload nginx

### Servir aussi la console d'administration

Remplacer le `location /` ci-dessus par les trois blocs suivants. Les préfixes
du backend sont déclarés AVANT le repli SPA, sinon `/p/` et `/api/` seraient
avalés par `try_files`.

    # 1. Le backend garde ses préfixes.
    location ~ ^/(api|p|l|healthz|readyz|robots\.txt) {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 90s;
        proxy_send_timeout 90s;
    }

    # 2. Les fichiers de la console, avec repli sur index.html pour ses routes
    #    internes (/domains, /audit, ...) qui n'existent pas sur le disque.
    root /var/www/difuzio-console;
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 3. Les assets portent une empreinte dans leur nom : cache long et sûr.
    #    index.html, lui, ne doit jamais être mis en cache, sinon un navigateur
    #    continue de charger les anciens assets après une mise à jour.
    #    Le `=404` est explicite à dessein : un asset absent doit donner un 404
    #    franc, jamais index.html (voir Points d'attention).
    location /assets/ {
        try_files $uri =404;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    location = /index.html {
        add_header Cache-Control "no-store";
    }

## Apache

`/etc/apache2/sites-available/difuzio.conf` :

    # difuzio - vhost frontal ; un bloc VirtualHost par base_url de domaine.
    <VirtualHost *:80>
        ServerName lists.example.org
        Redirect permanent / https://lists.example.org/
    </VirtualHost>

    <VirtualHost *:443>
        ServerName lists.example.org

        SSLEngine on
        SSLCertificateFile    /etc/letsencrypt/live/lists.example.org/fullchain.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/lists.example.org/privkey.pem

        # Transmettre le Host d'origine au backend.
        ProxyPreserveHost On
        ProxyPass        / http://127.0.0.1:8080/ timeout=90
        ProxyPassReverse / http://127.0.0.1:8080/

        # mod_proxy_http ajoute X-Forwarded-For et X-Forwarded-Host lui-même.
        RequestHeader set X-Forwarded-Proto "https"

        # L'API limite ses corps JSON à 1 MiB ; 2 MiB de marge au frontal.
        LimitRequestBody 2097152

        ErrorLog  ${APACHE_LOG_DIR}/difuzio-error.log
        CustomLog ${APACHE_LOG_DIR}/difuzio-access.log combined
    </VirtualHost>

Activation :

    a2enmod proxy proxy_http ssl headers
    a2ensite difuzio
    apache2ctl configtest && systemctl reload apache2

### Servir aussi la console d'administration

Remplacer le `ProxyPass / ...` global par un proxy limité aux préfixes du
backend, puis servir les fichiers statiques. `ProxyPass ... !` exclut
explicitement le reste de l'arborescence du proxy, et `FallbackResource` assure
le repli sur `index.html` pour les routes internes de la console. Aucun module
supplémentaire à activer : `FallbackResource` vient de `mod_dir`, actif par
défaut.

    # 1. Le backend garde ses préfixes (déclarés AVANT l'exclusion).
    ProxyPass        /api        http://127.0.0.1:8080/api        timeout=90
    ProxyPassReverse /api        http://127.0.0.1:8080/api
    ProxyPass        /p          http://127.0.0.1:8080/p          timeout=90
    ProxyPassReverse /p          http://127.0.0.1:8080/p
    ProxyPass        /l          http://127.0.0.1:8080/l          timeout=90
    ProxyPassReverse /l          http://127.0.0.1:8080/l
    ProxyPass        /robots.txt http://127.0.0.1:8080/robots.txt
    ProxyPass        /healthz    http://127.0.0.1:8080/healthz
    ProxyPass        /readyz     http://127.0.0.1:8080/readyz

    # 2. Tout le reste sort du proxy et vient du disque. Le repli sur
    #    index.html couvre les routes internes de la console (/domains,
    #    /audit, ...) qui n'existent pas sur le disque ; il n'agit que si
    #    aucun fichier ne correspond à l'URL demandée.
    ProxyPass / !
    DocumentRoot /var/www/difuzio-console
    <Directory /var/www/difuzio-console>
        Require all granted
        Options -Indexes
        FallbackResource /index.html
    </Directory>

    # 3. Un asset absent doit donner un 404 franc, jamais index.html : sinon le
    #    navigateur reçoit du HTML là où il attend du JavaScript et refuse de
    #    charger la console (voir Points d'attention).
    <Directory /var/www/difuzio-console/assets>
        FallbackResource disabled
    </Directory>

    # 4. Cache : long sur les assets empreintés, jamais sur index.html.
    <LocationMatch "^/assets/">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </LocationMatch>
    <Location "/index.html">
        Header set Cache-Control "no-store"
    </Location>

Ne PAS remplacer `FallbackResource` par la recette `mod_rewrite` que l'on
trouve couramment pour les SPA :

    # PIÈGE - ne fonctionne pas en contexte VirtualHost.
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^ /index.html [L]

En contexte serveur ou VirtualHost, `mod_rewrite` s'exécute pendant la phase de
traduction d'URL, avant le mappage vers le système de fichiers :
`%{REQUEST_FILENAME}` y vaut alors la même chose que `%{REQUEST_URI}`. Le test
`-f` porte donc sur `/assets/index-XXXX.js` à la racine du système de fichiers,
qui n'existe jamais, et la condition est vraie pour TOUTES les requêtes : le
serveur renvoie `index.html` même pour les fichiers réellement présents. Si
l'on tient à `mod_rewrite`, préfixer explicitement par la racine documentaire
(`RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f`), ou placer les règles dans
le bloc `<Directory>`, où `%{REQUEST_FILENAME}` est résolu.

## Points d'attention

- Certificats : les chemins ci-dessus supposent certbot
  (`certbot certonly --webroot` ou `--nginx`/`--apache`) ; tout autre client
  ACME convient, seuls les chemins changent.
- Le `POST /p/unsubscribe-oneclick` (désabonnement en un clic, RFC 8058) doit
  passer sans authentification ni challenge : si un WAF ou une protection bot
  est actif au frontal, l'exempter explicitement (voir
  [Installation](/difuzio/admin/installation)).
- Timeouts : les valeurs proxy (90 s ci-dessus) doivent rester au-dessus de
  `web.write_timeout` (60 s par défaut) pour ne pas couper une réponse avant le
  backend.
- Corps de requête : l'API refuse tout corps JSON au-delà de 1 MiB ; limiter à
  2 MiB au frontal protège le backend sans gêner l'usage normal.
- Console blanche et erreur de type MIME (`le chargement du module a été bloqué
  en raison d'un type MIME interdit (text/html)`) : le frontal renvoie
  `index.html` à la place du fichier `.js` ou `.css` demandé. Le type annoncé
  est correct, c'est le contenu qui est faux. Le diagnostic tient en une
  commande, à comparer avec la taille réelle de l'asset sur le disque :

        curl -sI https://lists.example.org/assets/index-XXXX.js

  Un `Content-Length` de quelques centaines d'octets sur un fichier qui en pèse
  des centaines de milliers signe un repli SPA qui avale les assets : soit
  `webui/dist/assets/` n'a pas été déployé (rsync sans récursion, mauvaise
  racine documentaire, droits de lecture manquants pour l'utilisateur du
  serveur web), soit la règle de repli se déclenche à tort - voir le piège
  `mod_rewrite` de la section Apache. Un asset manquant doit répondre 404, et
  ce 404 distingue immédiatement les deux cas.
- `/metrics` n'est pas sur ce port : il vit sur `metrics.bind`, surchargé par
  instance de worker par le template systemd (`127.0.0.1:909N` pour
  `worker@N`), et ne se proxifie pas publiquement (voir
  [Exploitation](/difuzio/admin/exploitation)).
- Les sondes `/healthz` et `/readyz` se consultent en local
  (`curl http://127.0.0.1:8080/readyz`) : la supervision n'a pas besoin de
  passer par le frontal.
- Adresse cliente : difuzio résout l'IP cliente depuis `X-Forwarded-For` quand
  la connexion vient d'un proxy déclaré dans `web.trusted_proxies` (le défaut
  couvre le loopback, donc les vhosts ci-dessus fonctionnent tels quels). Si le
  frontal est sur une autre machine, ajouter son adresse à la liste ; voir
  [Configuration](/difuzio/admin/configuration).
