C’est une situation que beaucoup connaissent. Tu organises un barbecue, les gamins débarquent avec leurs smartphones, et la première question avant même de dire bonjour c’est : “c’est quoi le WiFi ?”. Tu files le mot de passe du réseau guest à 25 caracteres + 4 changement de casse pour bien les entendre raler et là, le bonjour c’est sur, c’est mort mais c’est ma petite vengeance… Sinon isolé du LAN, accès internet uniquement, VLAN dédié — l’infra est propre. Sauf que t’as aucun contrôle sur ce qu’ils consultent.

J’aurais pu m’en foutre. Mais voilà, j’ai des convictions sur ce qui doit circuler sur mon réseau, et “laisser faire” n’en fait pas partie. Alors j’ai monté un filtrage DNS. Zéro budget, zéro dépendance externe, 100% sous contrôle.

Le contexte réseau

Mon réseau guest tourne sur un VLAN dédié (VLANXXX, 192.168.xxx.0/27), complètement isolé du LAN de production. Les clients n’ont accès qu’à internet, rien d’autre. Le WiFi “sérieux” pour les machines du LAN est sur un autre SSID entièrement — WPA2 Enterprise, SSID masqué, GPO pour l’accès des clients — mais c’est une autre histoire.

Le VLAN guest c’est le réseau public : tout le monde peut s’y connecter, personne ne peut en abuser. Du moins c’est l’objectif.

Pourquoi pas une solution cloud ?

Cloudflare for Families, NextDNS, OpenDNS — toutes des solutions valables sur le papier. Mais elles ont un point commun : tes requêtes DNS partent chez quelqu’un d’autre. Dans un contexte entreprise où tu gères des réseaux guest pour des intervenants externes, envoyer toutes leurs requêtes vers un tiers c’est une question de conformité qui mérite réflexion.

Et puis, il y a le principe. Si je peux faire ça en interne, pourquoi ne pas le faire en interne ?

L’architecture

Client VLANXXX
    ↓ requête DNS vers 192.168.XXX.xxx
Pi-hole (VM dédiée)
    ↓ domaine non bloqué → forward vers Unbound
Unbound (localhost:5335)
    ↓ résolution récursive
Root servers → TLD → authoritative
    ↓ réponse remonte
Pi-hole → client VLANXXX

Pi-hole fait le filtrage. Unbound fait la résolution récursive directement depuis les root servers, sans passer par aucun DNS externe. Les blocklists sont téléchargées périodiquement et stockées localement — si internet tombe, le filtrage continue.

VM dédiée ou conteneur ?

La question mérite d’être posée. Pi-hole est ultra-léger : 1 vCPU, 1 Go de RAM, 30 Go de disque virtuel, ça ne fait pas un pli dans les métriques d’un hyperviseur. Un LXC sur une VM existante serait parfaitement fonctionnel.

Mais dans mon infra, la philosophie c’est un service = une VM. Pas par dogmatisme, mais pour des raisons pratiques : Veeam sauvegarde chaque VM de façon indépendante, le PRA est prévisible, un snapshot avant update ne pollue pas un service voisin. Un LXC orphelin sur une VM existante c’est une dette opérationnelle qui se paie tôt ou tard.

Donc : VM dédiée. Debian 12 ou Ubuntu 24.04 LTS, au choix.

Installation d’Unbound

On installe Unbound en premier — Pi-hole en aura besoin comme upstream DNS.

sudo apt install unbound -y

On crée ensuite la configuration pour qu’Unbound écoute en local sur le port 5335 :

sudo nano /etc/unbound/unbound.conf.d/pi-hole.conf
server:
    verbosity: 0
    interface: 127.0.0.1
    port: 5335
    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    do-ip6: no
    prefer-ip6: no
    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: no
    edns-buffer-size: 1232
    prefetch: yes
    num-threads: 1
    so-rcvbuf: 1m
    private-address: 192.168.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8

On redémarre et on vérifie :

sudo systemctl restart unbound
sudo unbound-checkconf

Test de résolution :

dig google.com @127.0.0.1 -p 5335

La première requête prend 70-80ms (résolution depuis les root servers), les suivantes seront cachées. C’est le comportement attendu.

Installation de Pi-hole

Pi-hole propose un script d’installation interactif. On le télécharge d’abord pour pouvoir le lancer :

curl -sSL https://install.pi-hole.net -o install.sh
sudo bash install.sh

Note pour les environnements avec inspection SSL : si votre firewall fait de l’inspection SSL sur le trafic sortant, l’installeur peut bloquer sur la phase de téléchargement des dépendances (snapd valide les certificats Canonical avec certificate pinning). Dans ce cas, installez sans réseau et configurez l’IP manuellement après.

L’installeur pose quelques questions :

Interface d’écoute : sélectionnez l’interface côté réseau guest (dans notre cas eth1, 192.168.XXX.xxx).

Sélection de l’interface eth1

Upstream DNS : choisissez Custom et entrez 127.0.0.1#5335 — c’est notre Unbound local.

Configuration upstream DNS custom

Confirmation upstream DNS

Blocklist par défaut : acceptez StevenBlack’s Unified Hosts List. On ajoutera d’autres listes après.

Blocklists

Query logging : activez-le. C’est indispensable pour le debugging et pour vérifier que le filtrage fonctionne.

Activation du logging

Privacy mode : laissez sur “Show everything” pour avoir la visibilité complète.

Privacy mode

À la fin de l’installation, Pi-hole affiche le mot de passe généré pour l’interface web. Notez-le, vous pourrez le changer avec sudo pihole setpassword.

Installation complète

Pi-hole v6 : depuis la version 6, Pi-hole intègre son propre serveur web (FTL) et n’utilise plus lighttpd. Si vous avez installé lighttpd séparément au préalable, désactivez-le : sudo systemctl disable --now lighttpd && sudo systemctl restart pihole-FTL

L’interface web est accessible sur http://[IP]:80/admin :

Interface de login Pi-hole

Dashboard Pi-hole vide

Les blocklists

Pi-hole ne gère pas nativement les catégories — il travaille avec des listes de domaines. Le choix des listes détermine ce qui est filtré.

Stratégie de sélection

Plutôt que d’empiler des listes au hasard, voici une approche raisonnée par niveau :

Socle non négociable pour un réseau guest :

  • Malware et phishing
  • Contenu adulte/pornographie
  • Jeux d’argent

Raisonnable pour un réseau avec des mineurs :

  • Drogues
  • Contenu violent
  • Warez/piratage

Au choix selon le contexte :

  • Publicité et tracking (bonus privacy)
  • Réseaux sociaux (souvent contre-productif sur un guest)

Les listes UT1 (Université Toulouse Capitole)

L’Université Toulouse Capitole maintient depuis des années une liste noire catégorisée, largement utilisée dans les établissements scolaires français. Elle est disponible via un mirror GitHub maintenu par la communauté.

L’avantage : catégories claires, maintien actif, source française publique et auditable.

Note : certaines catégories comme adult et porn n’existent pas comme fichiers séparés sur ce mirror — elles sont remplacées par mixed_adult. Vérifiez toujours les URLs avant d’ajouter une liste.

En Pi-hole v6, les listes s’ajoutent via l’API. Commencez par récupérer un token d’authentification :

export PIHOLE_PASS='votre_mot_de_passe'
SID=$(curl -s -X POST http://localhost/api/auth \
  -H "Content-Type: application/json" \
  -d "{\"password\":\"$PIHOLE_PASS\"}" | python3 -c "import sys,json; print(json.load(sys.stdin)['session']['sid'])")

Puis ajoutez les listes UT1 :

for URL in \
  "https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/malware/domains" \
  "https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/phishing/domains" \
  "https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/gambling/domains" \
  "https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/warez/domains" \
  "https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/mixed_adult/domains" \
  "https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/drogue/domains" \
  "https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/dangerous_material/domains"
do
  curl -s -X POST "http://localhost/api/lists?type=block" \
    -H "Content-Type: application/json" \
    -H "sid: $SID" \
    -d "{\"address\":\"$URL\",\"comment\":\"UT1\"}"
done

La liste Hagezi NSFW

Pour le contenu adulte, les listes UT1 mixed_adult sont trop légères (149 domaines). La liste Hagezi NSFW est bien plus exhaustive — elle couvre 93 000+ domaines en format ABP, nativement supporté par Pi-hole v6 :

curl -s -X POST "http://localhost/api/lists?type=block" \
  -H "Content-Type: application/json" \
  -H "sid: $SID" \
  -d '{"address":"https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/nsfw.txt","comment":"Hagezi NSFW"}'

Blocage DoH

Le DNS over HTTPS est le principal vecteur de contournement d’un filtrage DNS. Les navigateurs modernes (Chrome, Firefox, Safari) l’activent par défaut et ignorent silencieusement le DNS du réseau.

On bloque les domaines DoH connus via la liste Hagezi dédiée :

curl -s -X POST "http://localhost/api/lists?type=block" \
  -H "Content-Type: application/json" \
  -H "sid: $SID" \
  -d '{"address":"https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/doh.txt","comment":"Hagezi DoH blocklist"}'

Important : cette liste bloque les domaines DoH, pas les IPs. Pour un blocage complet, il faut également bloquer TCP 443 vers les IPs des résolveurs DoH connus (1.1.1.1, 8.8.8.8, 9.9.9.9, etc.) au niveau du firewall. N’importe quel firewall — pfSense, OPNsense, FortiGate, Mikrotik — peut faire ça avec une règle “REJECT destination DoH-IPs service HTTPS”.

Mise à jour de gravity

Après avoir ajouté toutes les listes, on met à jour la base de données de filtrage :

sudo pihole -g

SafeSearch forcé

Pi-hole ne peut pas filtrer le contenu des pages — seulement bloquer des domaines. Pour Google Images et YouTube, on ne peut pas bloquer google.com sans bloquer Google entièrement.

La solution : forcer le mode SafeSearch via DNS. Google, Bing et YouTube exposent des endpoints dédiés SafeSearch. En redirigeant leurs domaines vers ces endpoints, le navigateur se retrouve sur la version filtrée sans pouvoir le contourner facilement.

On récupère d’abord l’IP de l’endpoint SafeSearch Google :

dig forcesafesearch.google.com +short
# 216.239.38.120

En Pi-hole v6, ça se configure dans /etc/pihole/pihole.toml, section hosts :

sudo nano /etc/pihole/pihole.toml

Trouvez la ligne hosts = [] (section DNS, vers la ligne 134) et remplacez par :

hosts = ["216.239.38.120 www.google.com", "216.239.38.120 www.google.fr", "216.239.38.120 www.youtube.com"]

Redémarrez FTL :

sudo systemctl restart pihole-FTL

Vérification :

dig www.google.com @192.168.XXX.xxx +short
# Doit retourner 216.239.38.120

Note : le SafeSearch forcé ne fonctionne correctement que si le client utilise le DNS Pi-hole. Avec le DoH actif côté client, la redirection est ignorée. D’où l’importance de bloquer le DoH à la fois par Pi-hole (domaines) et par le firewall (IPs).

Redirection DNS forcée au niveau firewall

Pour empêcher un client de contourner Pi-hole en configurant manuellement un DNS externe (8.8.8.8 par exemple), on ajoute une règle de redirection DNS au niveau du firewall.

Le principe est générique, applicable sur n’importe quel firewall :

  1. Créer un Virtual IP qui redirige tout trafic DNS (UDP/TCP 53) vers l’IP du Pi-hole (192.168.XXX.xxx)
  2. Créer une policy qui intercepte le trafic DNS du VLAN guest vers ce VIP
  3. Placer cette règle avant la règle d’accès internet normale

Ainsi, même un client qui tape 8.8.8.8 comme DNS verra ses requêtes silencieusement redirigées vers Pi-hole.

Résultat

Après configuration complète :

Dashboard Pi-hole en production

878 189 domaines bloqués répartis sur 9 listes :

  • StevenBlack Unified : 83 497 (ads, malware généraliste)
  • UT1 malware : 672 746
  • UT1 phishing : 672 580
  • UT1 gambling : 32 233
  • Hagezi NSFW : 93 911 (format ABP)
  • Hagezi DoH : 3 407
  • UT1 warez, drogue, dangerous_material : ~2 000

Le dashboard montre en temps réel les requêtes des clients, ce qui est bloqué, et depuis quel client. Utile pour vérifier le comportement et identifier les faux positifs éventuels.

Mise à jour automatique des listes

Pi-hole met à jour ses blocklists automatiquement via un cron qui tourne par défaut le dimanche à 1h du matin. Rien à configurer.

Pour les mises à jour de Pi-hole lui-même :

sudo pihole -up

Pour finir

En quelques heures de boulot, on a un filtrage DNS souverain, sans aucune dépendance externe, applicable à n’importe quelle infrastructure. La VM tourne avec 1 vCPU et 1 Go de RAM — ça ne se voit pas dans les métriques.

Le prochain barbecue se passe sous filtrage. Les ados auront internet, pas le reste. Et si l’un d’eux essaie de mettre 8.8.8.8 en DNS manuel, le firewall s’en occupe.

Dans un épisode suivant, on verra comment aller plus loin avec un portail captif open source pour les accès temporaires — le cas typique des intervenants externes en entreprise qui ont besoin d’un accès WiFi le temps d’une prestation.

Gestion quotidienne — faux positifs et blacklist manuelle

Le query log est l’outil central pour le suivi quotidien. Il liste toutes les requêtes DNS en temps réel avec le statut (bloqué/autorisé), le domaine, le client, et le temps de réponse.

Query log Pi-hole

Depuis ce log, deux boutons en bout de ligne permettent d’agir immédiatement :

  • Allow : ajoute le domaine à la whitelist — utile pour débloquer un faux positif (un service légitime bloqué par une liste trop agressive)
  • Deny : ajoute le domaine à la blacklist manuelle

Un clic sur “Deny” crée par défaut un Exact deny — le domaine exact est bloqué. On peut ensuite modifier l’entrée en Regex deny depuis la section Domains, ce qui couvre le domaine racine et tous ses sous-domaines potentiels. Pour un domaine comme porn.com, le Regex deny est préférable.

Blacklist manuelle avec Regex deny

La whitelist fonctionne sur le même principe — indispensable quand une blocklist trop large touche un service légitime. Le query log permet d’identifier rapidement le domaine à débloquer sans avoir à désactiver toute une liste.