Un serveur DNS-over-TLS ft. Docker, Traefik, Pi-hole, Unbound

Dans cet article nous allons voir comment créer son propre serveur DNS et comment communiquer avec celui-ci avec le protocole DNS-over-TLS. Je souhaitais également raccorder le tout à un Pi-hole permettant ainsi de bloquer les nuisances d'Internet...

Un serveur DNS-over-TLS ft. Docker, Traefik, Pi-hole, Unbound
Cet article a un intérêt purement démonstratif. Ce n'est pas parce que c'est faisable que vous devez le faire : utiliser un serveur DNS personnel vous rend responsable de sa sécurité, et peut vous rendre plus unique.

Préambule

Cet article n'a pas pour but de vous ré-expliquer ce qu'est un DNS (qu'est-ce que le DNS ?). Pour faire court, le DNS (Domain Name System) permet de se connecter à des machines sans utiliser leurs adresses IP directement. C'est un protocole aujourd'hui indispensable et donc critique en termes de sécurité.

Mais ce qui nous intéresse en particulier c'est effectivement le DNS-over-TLS, un protocole qui encapsule les requêtes DNS dans la couche TLS (Transport Layer Security). Vous utilisez TLS en ce moment-même, en vous connectant à mon blog avec le protocole HTTPS (HTTP + TLS). Imaginez donc la même chose, mais avec les DNS.

Aujourd'hui, on voit de moins de moins du HTTP non-sécurisé par TLS, et les navigateurs modernes commencent à marquer les sites sans HTTPS comme non-sûrs. La raison est que, sans TLS donc sans chiffrement, vos communications de votre ordinateur à la machine sont en clair et n'importe qui peut sur le chemin peut les intercepter (c'est ce qu'on appelle communément une man in the middle) voire les altérer (attaque DNS spoofing, un type de MITM).

Illustration de Cloudflare

À l'image de HTTPS, il en va de même pour le DNS : il serait bien temps de chiffrer une partie aussi sensible, non ?

Longtemps DNSCrypt (par Frank Denis & Yecheng Fu) a été une des seules solutions efficaces, mais des solutions ont été standardisées depuis, et commencent à être implémentées :

  • DNS-over-TLS (DoT)
  • DNS-over-HTTPS (DoH)
Illustration de Cloudflare

L'un et l'autre ont leurs avantages et inconvénients. L'avantage de DoH c'est qu'il utilise le port 443 comme HTTPS, donc il sera difficile de le filtrer contrairement au port 853 utilisé par DoT. DoT a également certains avantages techniques mais ce n'est pas le sujet de cet article.

Pour ma part, je préfère DoT car Android l'implémente nativement depuis Android Pie (9.0). C'est la feature Private DNS qui prime sur tout, même si vous avez un VPN.

Notes sur l'usage d'un VPN avec DoT (ou même DoH) :

  • Idéalement, si vous utilisez un VPN de confiance qui fournit ses propres DNS sur le même serveur que le VPN, vous n'avez pas besoin de DoT car ce serait redondant (le chiffrement de vous jusqu'au VPN, incluant donc les requêtes DNS, a déjà lieu).
  • Utiliser un serveur DNS custom avec un VPN peut aussi vous rendre plus unique ; mais vous pouvez utiliser ce DNS pour bloquer des domaines malicieux avec Pi-hole, ce qui est le but de l'article.

Il est aussi à noter que bien que DoT/DoH augmentent considérablement la sécurité de vos requêtes DNS, il y a encore des progrès à faire en matière de vie privée notamment avec Encrypted SNI.

Objectif

L'objectif du setup que je propose vous permettra :

  • D'héberger votre propre serveur DNS récursif, avec Unbound. Un serveur DNS récursif remplace tout autre service DNS commercial en faisant le même travail : récupérer les domaines à partir des serveurs racine.
  • De bloquer les nuisances courantes, avec le bien connu Pi-hole. Vous pouvez filtrer au niveau DNS les publicités, les trackers... Utile si vous voulez garder un Android rootless (préférable en sécurité).
  • De chiffrer la communication entre le client (vous) et le serveur, avec Traefik et son routing TCP rendu possible depuis la v2. Traefik gère également la couche TLS et génère/renouvelle le certificat automatiquement avec Let's Encrypt.

Vous voyez, ce n'est pas si compliqué !

Bon, OK, j'ai des progrès à faire en infographie !

Motivation personnelle

Sachez d'abord qu'il existe déjà des services tels que AdGuard ou l'excellent NextDNS qui proposent du DNS-over-TLS/HTTPS en plus de bloquer des nuisances. NextDNS se rapproche d'ailleurs le plus de ce que vous obtiendrez à la fin de cet article, c'est-à-dire un Pi-hole dans le cloud.

Seulement, voilà :

  • Vous devez leur faire confiance avec vos données de requêtes DNS.
  • NextDNS est freemium, et limité à 300 000 requêtes/mois en gratuit.
  • Tout est plus amusant quand on le fait soi-même...

Cela dit, en termes de fiabilité et de performances, AdGuard et NextDNS sont très solides et probablement resteront plus performants que votre DNS récursif personnel, pour la simple et bonne raison que des tas d'utilisateurs utilisent ces services ce qui aide à remplir leur cache ; tandis que votre petit DNS récursif devra souvent envoyer des requêtes aux serveurs DNS racines, qui sont parfois lents à répondre.

En termes de sécurité et de vie privée, au moins vous êtes sûrs que vous êtes le maître de vos données. Mais cela ne vous exempt pas de configurer rigoureusement votre serveur en matière de sécurité. Un serveur compromis est la pire intrusion possible en termes de vie privée, comparable au fait-même d'utiliser Facebook.

J'utilise ce setup depuis plus de deux semaines et j'ai "oublié" que j'utilisais mon propre serveur DNS : c'est un très bon signe !

Pré-requis

Routage TCP avec Traefik

Depuis sa version 2, Traefik supporte outre le routage classique HTTP/HTTPS deux nouveaux types de routage : UDP et TCP. DNS-over-TLS est un type de communication reposant sur TCP donc c'est ce dernier qui nous intéressera.

Il faut commencer d'abord par ouvrir le port 853 sur l'hôte et le mapper avec le conteneur Traefik. Dans un docker-compose, ça ressemble à ceci :

    ports:
      - 80:80
      - 443:443
      - 853:853
docker-compose.yml

Désormais il va falloir modifier la configuration statique de Traefik pour rajouter un entrypoint (point d'entrée) à l'image de ce que vous avez déjà pour HTTP/HTTPS, mais cette fois-ci pour DoT :

entryPoints:
  http:
    address: ":80"
  https:
    address: ":443"
  dot:
    address: ":853"
traefik.yml

Boom, on redémarre Traefik et on constate dans la dashboard que c'est bien configuré comme il faut :

Fire up the containers!

Rien de bien compliqué, je vais vous donner les configurations que j'utilise. Mais je vous invite très fortement à vous renseigner sur chaque conteneur que vous lancez pour configurer par vous-même ou au moins, en connaissance de cause...

Pi-hole

  pihole:
    image: pihole/pihole
    container_name: pihole
    domainname: dns.domain.tld
    restart: unless-stopped
    networks:
      - main_network
    depends_on:
      - unbound
    dns:
      - 127.0.0.1
      - 1.1.1.1
    volumes:
      - /mnt/docker/pihole/etc-pihole:/etc/pihole
      - /mnt/docker/pihole/etc-dnsmasqd:/etc/dnsmasq.d
    environment:
      - TZ=Europe/Berlin
      - VIRTUAL_HOST=dns.domain.tld
      - WEBPASSWORD=xxxxxxxxxxxxxxx
      - DNS1=<IP DU CONTENEUR UNBOUND>#5053
      - DNS2=<IP DU CONTENEUR UNBOUND>#5053
      - DNSMASQ_USER=pihole
    labels:
      - traefik.enable=true
      - traefik.docker.network=main_network
      
    ### Web UI (HTTP/HTTPS)
      # - traefik.http.routers.pihole.entrypoints=http
      # - traefik.http.routers.pihole.rule=Host(`dns.domain.tld`)
      # - traefik.http.routers.pihole.middlewares=https-redirect@file
      - traefik.http.routers.pihole-secure.entrypoints=https
      - traefik.http.routers.pihole-secure.rule=Host(`dns.domain.tld`)
      - traefik.http.routers.pihole-secure.tls=true
      - traefik.http.routers.pihole-secure.tls.certresolver=http
      # - traefik.http.routers.pihole-secure.middlewares=secure-headers@file,hsts-headers@file,admins-auth@file
      - traefik.http.routers.pihole-secure.service=pihole-web
      - traefik.http.services.pihole-web.loadbalancer.server.port=80
   
    ### DNS-over-TLS (853 Traefik -> 53 Pi-hole)
      - traefik.tcp.routers.pihole-dot.rule=HostSNI(`dns.domain.tld`)
      - traefik.tcp.routers.pihole-dot.entrypoints=dot
      - traefik.tcp.routers.pihole-dot.tls.certresolver=http
      - traefik.tcp.routers.pihole-dot.service=pihole
      - traefik.tcp.services.pihole.loadbalancer.server.port=53
docker-compose.yml

Vous devez changer :

  • /mnt/docker par l'endroit sur l'hôte où vous voulez monter.
  • main_network par le réseau que vous utilisez pour Traefik.
  • Les occurrences de dns.domain.tld par le domaine souhaité.
  • Les occurrences de <IP DU CONTENEUR UNBOUND>, cf. suite.
  • La variable d'environnement WEBPASSWORD.
  • Les labels Traefik selon votre configuration (changement de noms).
  • Les middlewares Traefik que j'ai commentés car à adapter.

Pour ces derniers, je vous suggère en tous cas une authentification basique. Bien que l'accès administrateur soit verrouillé, ça évite que votre Pi-hole se retrouve listé dans shodan par exemple, et aussi que vos stats globales soient visibles par tout le monde.

Unbound

  unbound:
    image: klutchell/unbound
    container_name: unbound
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      main_network:
        ipv4_address: <IP FIXE DU CONTENEUR>
    volumes:
      - /mnt/docker/unbound/unbound.conf:/opt/unbound/etc/unbound/unbound.conf
docker-compose.yml

Vous devez changer :

  • /mnt/docker par l'endroit sur l'hôte où vous voulez monter.
  • <IP FIXE DU CONTENEUR> par une IP fixe du subnet attribué à main_network (en cohérence avec précédemment).

Cette image d'Unbound est appréciée car elle ne tourne pas en root, est à jour, et fonctionne out-of-the-box comme DNS récursif.

En réalité, vous n'êtes pas obligé de monter ce volume, la configuration par défaut fonctionne. Mais j'ai apporté quelques modifications pour ma part :

server:
    interface: 0.0.0.0@5053
    do-ip4: yes
    do-ip6: no
    do-udp: yes
    do-tcp: yes
    do-daemonize: no
    access-control: 127.0.0.1/32 allow
    access-control: 192.168.0.0/16 allow
    access-control: 172.16.0.0/12 allow
    access-control: 10.0.0.0/8 allow
    auto-trust-anchor-file: /var/run/unbound/root.key
    logfile: ""
    verbosity: 1
    statistics-interval: 600
    statistics-cumulative: yes
    include: /opt/unbound/etc/unbound/a-records.conf
    cache-max-ttl: 86400
    cache-min-ttl: 3600
    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: no
    num-threads: 4
    msg-cache-size: 256m
    rrset-cache-size: 256m
    infra-cache-numhosts: 100000
    prefetch: yes
    edns-buffer-size: 1472
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8
    private-address: fd00::/8
    private-address: fe80::/10
unbound.conf

Ces modifications permettent un gain de performance par le biais notamment d'un caching assez agressif (car oui, de base ça sera plus lent que les DNS récursifs de Cloudflare par exemple, et vous n'y pouvez rien).

J'ai apporté également quelques modifications relatives à la sécurité (dont DNSSEC). Encore une fois vous pouvez juste utiliser la configuration par défaut qui fonctionne, et bien sûr consulter la documentation officielle.

Lancement

À ce stade, vous avez simplement à faire docker-compose up -d.

Considérations

Restreindre l'accès DNS-over-TLS

Oui, par défaut, n'importe qui avec votre domaine pourrait utiliser votre serveur DNS-over-TLS. Contrairement aux serveurs DNS classiques, les attaques par amplification ne devraient pas être un problème de toute façon. Et dans ce "tutoriel", nous avons exposé publiquement uniquement le port 853.

Toujours est-il que vous n'êtes pas forcément à l'aise à cette idée ! Malheureusement, Traefik ne propose pas des whitelists d'IP pour son mode de routage TCP (car le concept de middlewares ne s'applique vraiment qu'à son routage HTTP apparemment...).

  • Une solution est de recourir à iptables, et c'est devenu plus simple avec Docker depuis le temps, mais j'en ferai un article à part.
  • Une autre solution est de se passer de Traefik et du DNS-over-TLS pour héberger un serveur WireGuard sur le même serveur que Pi-hole et Unbound. Radical (dépasse le cadre de cet article) mais ça marche aussi.

Au moins, je vous donne cette piste !

Pi-hole ne verra pas l'IP réelle

Pi-hole ne verra que l'IP de Traefik, car dans un routing TCP on ne peut pas vraiment faire usage de headers comme X-Forwarded-For. Je suis actuellement à la recherche d'une solution à ce "problème", mais après je n'ai pas réellement besoin d'une gestion précise de mes clients donc ça ne me gêne pas plus que ça.

Utilisation & configuration

Accès à la Web UI Pi-hole

Votre interface Pi-hole sera disponible à l'adresse que vous avez configurée, dans mon exemple dns.domain.tld.

Configurer Pi-hole

Une fois dans votre interface admin, vous n'avez pas vraiment autre chose à configurer que vos listes de blocage. Les DNS upstream sont forcés par les variables d'environnement DNS1 et DNS2.

Concernant les listes de blocage, je vous conseille celles-ci en plus de celles activées par défaut :

Utiliser sur Android (9.0+)

Depuis Android Pie, vous pouvez utiliser DNS-over-TLS sans installer une application ou passer par une option VPN (iOS je te regarde !).

FR : Paramètres réseau > Avancé > DNS Privé
EN : Network & Internet > Advanced > Private DNS

Activez l'option et rentrez dans le champ dns.domain.tld. Ça y est ! Si vous voyez "couldn't connect" à ce moment-là, quelque chose ne va logiquement pas ; autrement, on dirait bien que ça marche.

Utiliser sur macOS/Linux

Il est possible d'utiliser DNS-over-TLS avec stubby. Je vous laisse vous renseigner dessus. Une fois stubby configuré pour pointer sur dns.domain.tld, changez vos DNS système sur 127.0.0.1 (localhost).

Utiliser sur Windows

Franchement je n'en ai aucune idée, you're on your own...

... vérifier que tout fonctionne ?

L'avantage du DNS c'est que vous verrez plus ou moins tout de suite si ça marche... ou pas. Premièrement, vous pouvez consulter l'activité DNS (blocages des nuisances y compris) dans votre Pi-hole, et également les statistiques de Unbound avec docker logs unbound.

Outre dig, vous pouvez vérifier que le DNS fonctionne avec ces services :

Conclusion

Si vous avez suivi cet article, félicitations, vous utilisez votre propre serveur DNS qui ne dépend que des serveurs racines et qui bloque les nuisances, et vous communiquez avec celui-ci de façon chiffrée.

Je pense avoir bien abordé le sujet, même s'il y a d'autres choses à dire évidemment.

Have fun & stay safe!