Cet article documente le service de proxy transparent : le routage PBR, l'interception HTTPS, les deux politiques de déchiffrement, et la chaîne d'inspection antivirus. Les trois images FROM scratch qui l'exécutent ont le leur.
Architecture du flux
flowchart LR
subgraph iot["VLAN IoT"]
device["Appareil IoT"]
end
subgraph vyos["Routeur"]
pbr["PBR table 100"]
nft["nft REDIRECT<br/>dans le netns du proxy"]
end
subgraph proxy["Container proxy"]
squid["Squid<br/>peek+splice"]
end
subgraph icap["Container ICAP"]
cicap["c-icap<br/>REQMOD + RESPMOD"]
end
subgraph av["Container antivirus"]
clam["clamd<br/>analyse"]
end
device --> pbr
pbr --> nft
nft -->|"80 et 443 rediriges"| squid
squid -->|"ICAP"| cicap
cicap -->|"protocole clamd"| clam
clam -->|"sain"| cicap
cicap -->|"OK"| squid
squid --> dest["Destination"]
Policy-Based Routing et injection nft
Le VLAN des objets connectés est routé par PBR vers le container du proxy : les paquets sont marqués par des règles nftables et dirigés vers le bridge réseau du container.
Un service systemd, lié au container, injecte les règles de redirection dans le
namespace réseau du container : le port 80 vers le port d'interception HTTP, le
443 vers celui d'interception HTTPS. Squid retrouve ensuite la destination
d'origine par SO_ORIGINAL_DST.
Ce service est le point critique de tout le montage. Sans lui, le trafic PBR arrive bien dans le namespace du container, mais rien n'écoute sur les ports 80 et 443 : les paquets sont jetés en silence, et tous les objets connectés perdent HTTPS sans qu'aucune alerte ne se déclenche.
Les quatre ports, et ce qui les distingue
Squid écoute sur quatre ports, et c'est le port d'entrée qui détermine le traitement :
| Port | Mode | Clients | Traitement |
|---|---|---|---|
| 3128 | explicite | serveurs, LXC, KVM | proxy HTTP classique |
| 3129 | interception | objets connectés | HTTP redirigé depuis le port 80 |
| 3130 | explicite | serveurs, LXC, KVM | HTTPS déchiffré, journal par URL complète |
| 3131 | interception | objets connectés | HTTPS en tunnel, journal par SNI |
Le format de journal inclut le port d'écoute, ce qui permet de distinguer les flux dans la supervision — sans quoi les quatre usages se mélangent dans un seul fichier.
SSL Bump recouvre deux comportements opposés
C'est la source de confusion la plus fréquente sur ce montage : le terme désigne ici deux politiques contraires, et c'est le port qui tranche.
Sur le port d'interception (3131) — peek puis splice ou terminate :
- peek lit le nom de serveur annoncé dans le ClientHello, sans déchiffrer
- splice laisse ensuite passer le trafic en tunnel, sans inspection du contenu
- terminate coupe ce qui n'est pas autorisé
Le trafic des objets connectés est donc filtré par domaine et journalisé, mais jamais déchiffré. Aucun appareil ne voit de certificat inhabituel — ce qui compte, parce que beaucoup d'objets refusent toute autorité autre que celle qu'ils embarquent.
Sur le port explicite (3130) — bump complet :
http_port 3130 name=explicit_bump ssl-bump \
cert=/etc/squid/ssl_cert/bump.pem \
generate-host-certificates=on
ssl_bump splice nobump_dst port_explicit_bump
ssl_bump bump port_explicit_bump
Squid génère un certificat à la volée pour chaque destination, déchiffre, inspecte, rechiffre. C'est une interception assumée : elle donne le journal par URL complète au lieu du seul nom de serveur, et elle rend le contenu HTTPS visible de l'antivirus. Les hôtes concernés font confiance à l'autorité interne, et les domaines qui ne tolèrent pas l'opération — épinglage de certificat, banques, santé — sont listés pour retomber en tunnel.
Le périmètre réel de l'antivirus découle de la politique de déchiffrement
C'est la conséquence qu'on oublie le plus vite. L'antivirus voit tout le HTTP en clair et tout le HTTPS déchiffré par le port explicite. Ce qui reste en tunnel — tout le trafic des objets connectés, et les domaines exclus du déchiffrement — est opaque, et ne sera jamais analysé.
Autrement dit : le VLAN qui justifie le plus l'analyse antivirus est précisément celui qui en bénéficie le moins. C'est un compromis assumé, pas un oubli — mais il doit être su, parce qu'un schéma qui montre une flèche vers l'antivirus laisse croire l'inverse.
Les flux en continu ne survivent pas à l'adaptation ICAP
L'adaptation ICAP suppose une réponse qui progresse. Un flux text/event-stream
qui reste silencieux pendant que le serveur travaille ne progresse pas : l'échange
ICAP expire, Squid abandonne la réponse et sert sa propre page d'erreur HTML à
la place du flux.
Le cas mesuré ici concerne une API de génération de texte, dont les pauses dépassent régulièrement le délai d'attente ICAP. Le client affiche une erreur qui désigne le service distant, et rien ne pointe vers le proxy — c'est ce qui rend le diagnostic long.
La signature, elle, est sans ambiguïté dans les journaux : un plancher de durée
égal au délai ICAP sur toutes les occurrences, avec une réponse en text/html
là où le flux normal renvoie text/event-stream.
Deux enseignements dépassent le cas particulier :
bypass=1ne rattrape pas ce cas. Il laisse passer la requête quand le service ICAP est injoignable, mais quand la réponse est déjà engagée et que le délai tombe en cours de flux, Squid n'a plus de porte de sortie.- Resserrer les délais ICAP a un coût. Les valeurs par défaut sont très larges ; les réduire pour éviter qu'un client attende des minutes rend mécaniquement ce type de flux incompatible avec l'adaptation.
La parade est d'exclure ces domaines de l'adaptation, pas du déchiffrement :
acl api_streaming dstdomain .exemple-api.tld
adaptation_access service_req deny api_streaming
adaptation_access service_resp deny api_streaming
Le trafic reste déchiffré et journalisé par URL ; seule l'analyse antivirus est
retirée — elle n'apporte rien sur un flux authentifié qui ne transporte pas de
fichier. L'ordre compte : ces deny doivent précéder les allow all.
Même serveur DNS ne veut pas dire même réponse
Le proxy et ses clients doivent obtenir la même adresse pour un nom donné, sans quoi Squid conclut à une usurpation d'en-tête et refuse la requête. Une règle DNAT intercepte donc aussi le DNS du container et le dirige vers le résolveur interne : proxy et clients interrogent le même serveur.
Condition nécessaire, pas suffisante — et c'est tout l'intérêt du cas. Le résolveur alloue un cache par vue, et il en compte près d'une vingtaine. Le client et le proxy tombant dans deux vues distinctes, chacun recevait sa propre résolution d'un CDN en répartition tournante : deux listes d'adresses disjointes, servies par le même serveur, au même instant.
Le symptôme est trompeur : un téléchargement d'image échoue avec
http: server gave HTTP response to HTTPS client, parce que Squid renvoie sa page
d'erreur en clair sur une connexion TLS.
Le correctif est côté résolveur, pas côté proxy — un cache partagé entre les vues récursives. Le détail est dans l'architecture split-horizon.
Une vérification retirée plutôt que désactivée
host_verify_strict n'est pas mise à off : la directive est retirée de la
configuration. Active, elle bloque les requêtes vers les CDN dès que l'adresse
résolue par le proxy diffère de celle du client.
Il faut dire ce que c'est : un contournement de symptôme, posé quand la cause était encore attribuée à la seule répartition tournante des CDN. La cause structurelle — les caches par vue — a été traitée plus tard, ailleurs. Les deux mesures coexistent aujourd'hui, et couvrent deux choses différentes : le retrait couvre la répartition tournante légitime, le cache partagé supprime la divergence entre vues.
Garder les deux est un choix, mais garder le premier en croyant qu'il traite la seconde en serait un mauvais.
Le démarrage, là où ça casse silencieusement
Les fichiers d'unité personnalisés vivent dans la configuration persistante du routeur et sont liés symboliquement vers l'emplacement attendu par systemd. Au démarrage, systemd tente de les charger avant que cette configuration soit montée : le lien pointe dans le vide, le service reste inactif, aucune règle de redirection n'est posée, et les objets connectés perdent HTTPS sans un mot.
Deux mécanismes complémentaires ferment la porte :
- une dépendance explicite au montage dans le fichier d'unité ;
- un script exécuté après le démarrage, qui recrée les liens, recharge systemd, puis active et démarre chaque service.
Le même script rétablit la route PBR et contourne la traduction d'adresse automatique du réseau de containers, qui sans cela réécrirait le trafic marqué par le PBR.
Surveiller le maillon invisible
Le service de redirection est exactement le genre de composant dont la panne ne se voit pas : rien ne tombe, le trafic est simplement jeté. Un contrôle périodique vérifie donc trois choses distinctes, et pas seulement la première :
- que le service de redirection est actif ;
- que le container du proxy tourne ;
- que les règles de redirection sont réellement présentes dans le namespace réseau.
C'est la troisième qui compte. Les deux premières peuvent être vertes pendant que la redirection a disparu — un service « actif » n'est pas une preuve que son effet existe encore.
Le résultat est poussé vers la supervision depuis l'intérieur du container, ce qui évite d'ouvrir un chemin sortant supplémentaire dans le pare-feu. La détection d'une panne tient en moins de deux minutes.