La chaîne de reverse proxy
Chaque requête entrante traverse cinq couches avant d'atteindre l'application. Chacune a un rôle unique, et c'est cet ordre précis qui compte : l'inspection vient avant le cache, faute de quoi une réponse déjà en cache serait servie sans jamais repasser par le contrôle.
sequenceDiagram
participant Client as Client Internet
participant HAP as HAProxy<br/>terminaison TLS
participant TFK as Traefik<br/>Gateway API
participant WAF as WAF nginx<br/>ModSecurity CRS
participant VRN as Varnish<br/>cache HTTP
participant WP as Application<br/>nginx + php-fpm
Client->>HAP: HTTPS
Note over HAP: routage par SNI
HAP->>TFK: HTTPS re-chiffre<br/>certificat interne verifie
Note over TFK: route unique<br/>filtre sur l adresse source
TFK->>WAF: HTTP
Note over WAF: inspection du contenu<br/>par domaine
alt Requete refusee
WAF-->>TFK: 403
else Requete acceptee
WAF->>VRN: transmission
alt Present en cache
VRN-->>WAF: reponse depuis la memoire
else Absent du cache
VRN->>WP: requete au backend
WP-->>VRN: reponse
end
VRN-->>WAF: reponse
end
WAF-->>TFK: reponse
TFK-->>HAP: reponse
HAP-->>Client: HTTPS
Deux conséquences de cet ordre, qui ne se lisent pas sur le schéma :
- Le pare-feu applicatif voit tout le trafic, le cache n'en voit qu'une partie. Une réponse servie depuis le cache ne touche pas l'application, mais elle a déjà traversé l'inspection.
- Le pare-feu applicatif est aussi le routeur. C'est lui qui porte la seule route du cluster pour les sites web : Traefik ne connaît pas Varnish, il ne connaît que le service du pare-feu, qui redirige ensuite vers le cache.
HAProxy : terminaison TLS et routage par SNI
HAProxy tourne en conteneur sur le routeur, dans un réseau dédié. C'est le premier point de contact du trafic entrant.
Sélection du certificat
Une liste associe chaque fichier de certificat aux domaines qu'il couvre, et la sélection se fait sur le nom annoncé par le client pendant la négociation TLS :
# Format : chemin_pem [options] domaine [domaine2...]
/etc/haproxy/WWW/blog.pem blog.exemple.fr www.exemple.fr
/etc/haproxy/WWW/app.pem app.exemple.fr
/etc/haproxy/WWW/home.pem home.exemple.fr
C'est le mécanisme qui rend la panne décrite plus bas si brutale : la liste est validée en entier au démarrage.
Routage par domaine
frontend ft_https
bind *:443 ssl crt-list /etc/haproxy/WWW.crt_list alpn h2,http/1.1
mode http
# Normalisation de l en-tete Host
http-request set-var(txn.txnhost) hdr(host),lower,regsub(:[0-9]+$,"")
acl host_blog var(txn.txnhost) -m str blog.exemple.fr
acl host_app var(txn.txnhost) -m str app.exemple.fr
use_backend bk_traefik if host_blog
use_backend bk_traefik if host_app
La normalisation de l'en-tête — minuscules, suppression du port — évite qu'une requête échappe au routage pour une différence de casse.
Le trafic interne est re-chiffré
backend bk_traefik
mode http
timeout connect 30s
timeout server 600s
timeout tunnel 3600s
server traefik1 10.0.2.101:30443 ssl \
verify required \
ca-file /etc/ssl/internal-ca.crt \
sni str(traefik.internal)
Le trafic entre les deux couches est re-chiffré avec vérification du certificat interne. Le nom envoyé pendant la négociation est fixe : le routage applicatif se fait sur l'en-tête HTTP, pas sur le nom TLS.
Pourquoi se donner cette peine sur un réseau interne ? Parce que « interne » n'est pas une propriété de sécurité : si un conteneur voisin est compromis, du trafic en clair est du trafic lisible. La vérification garantit en plus qu'on parle bien à la bonne couche, et pas à quelque chose qui a pris sa place.
Traefik : route unique et filtrage sur la réputation
Une seule route pour tous les sites
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: waf
spec:
hostnames:
- "blog.exemple.fr"
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: ExtensionRef
extensionRef:
kind: Middleware
name: crowdsec-bouncer
backendRefs:
- name: waf
port: 80
La destination désigne le pare-feu applicatif, pas le cache : c'est
précisément ce qui met l'inspection avant Varnish. Tous les sites du cluster
partagent cette route unique, et le tri par domaine se fait ensuite dans les
blocs server du pare-feu.
Un filtre vérifie l'adresse source contre une base de réputation avant même que la requête n'atteigne le pare-feu applicatif. Les deux filtrages ne font pas le même travail : l'un juge qui appelle, l'autre ce qu'il envoie.
L'ordre inspection puis cache n'est pas négociable
Le pare-feu applicatif reçoit le trafic et le renvoie vers le cache ; celui-ci est donc derrière l'inspection, jamais devant.
L'ordre inverse aurait une conséquence simple et durable : une page mise en cache serait resservie à toutes les requêtes suivantes sans que le pare-feu les voie passer — et une règle ajoutée ensuite ne s'appliquerait qu'aux requêtes absentes du cache.
L'inspection se règle par domaine, pas globalement : chaque bloc server
porte son propre interrupteur. Un domaine peut donc traverser la couche sans être
inspecté du tout pendant que ses voisins le sont, et le fichier de configuration
a l'air normal dans les deux cas.
Ce que ça a coûté ici
Le domaine du blog a porté un modsecurity off; pendant trois mois, avec en
commentaire « géré par le nginx du pod applicatif ». C'était vrai à l'écriture.
Trois semaines plus tard, un autre commit a retiré ModSecurity de ce pod, au motif que « le pare-feu central s'en charge en amont ». Chaque changement était défendable seul ; leur composition a laissé le domaine sans aucune inspection, et aucun des deux fichiers ne devenait faux — chacun décrivait une délégation vers l'autre.
La leçon tient en une phrase : une couche de sécurité se vérifie par une requête, pas par une lecture de configuration. Une sonde qui envoie une charge que le moteur doit refuser, et qui exige ce refus, aurait signalé le trou le jour même. Aucune relecture de fichiers ne l'aurait trouvé. Le principe et sa mise en œuvre sont dans la supervision.
Les délais d'attente doivent être alignés sur toute la chaîne
Un délai plus court en amont coupe la connexion en cours de transfert et produit des erreurs qui désignent la mauvaise couche.
| Couche | Paramètre | Ordre de grandeur |
|---|---|---|
| HAProxy, côté client | timeout client |
quelques minutes |
| HAProxy, côté backend | timeout server |
identique |
| HAProxy, connexions longues | timeout tunnel |
nettement supérieur |
| Traefik | lecture et écriture | identiques aux précédents |
| Pare-feu applicatif | lecture et écriture vers l'amont | identiques |
| Backend | lecture vers l'amont | identiques |
Le mécanisme de panne mérite d'être connu, parce qu'il est trompeur : un délai court en amont coupe la connexion pendant un envoi, la couche suivante reçoit un corps tronqué, le backend répond une erreur de requête malformée, et l'entrée voit une coupure qu'elle rapporte comme une panne de passerelle. Trois couches signalent trois erreurs différentes, et aucune ne pointe celle qui a coupé.
Deux pannes qui ne se limitent pas à leur cause
Un certificat manquant met tous les sites hors ligne. La liste de certificats est validée dans son intégralité au démarrage, et un seul fichier absent empêche le processus de démarrer. La panne ne se limite donc pas au domaine fautif : tout tombe ensemble. C'est la raison pour laquelle un certificat se génère et se vérifie avant d'être référencé, jamais l'inverse.
Une connexion maintenue ouverte est coupée comme une requête ordinaire. Une connexion promue en WebSocket reste ouverte sans échanger de données pendant de longues périodes. Elle est comptée comme une requête HTTP normale et coupée au bout du délai serveur ; seul un délai dédié aux connexions longues la sort de ce compteur une fois la promotion acceptée.
Deux autres pièges de cette chaîne se corrigent ailleurs, et sont documentés là où ils se règlent : une règle ajoutée ne s'applique pas tant que le processus n'a pas rechargé sa configuration — voir le cluster K3s — et la sonde du cache doit viser un endpoint dédié plutôt que la racine, expliqué dans le routage VCL.