BIND9 Hardened : DNS autoritatif FROM scratch

Rôle dans l'architecture

BIND9 est le serveur DNS central du homelab. Il gère à la fois les zones internes (.home.arpa) et la zone publique, avec un système de vues (split-horizon) qui adapte les réponses selon le réseau source du client.

flowchart LR
    subgraph Internes
        C["Clients internes"]
    end
    subgraph Externes
        E["Requetes externes"]
    end
    subgraph BIND9
        VS["Vue per-subnet<br/>recursion autorisee"]
        VL["Vue local<br/>zones completes"]
        VI["Vue internet<br/>autoritaire uniquement"]
    end
    C -->|"DNAT rule"| VS
    C -->|"DNAT rule"| VL
    E -->|"WAN DNAT"| VI

Tout le DNS interne est intercepté par une règle DNAT sur le routeur : aucun client ne sort en DNS directement vers Internet.

L'image est compilée depuis les sources ; les options de compilation et les drapeaux de durcissement sont dans le Dockerfile du dépôt. Cet article ne traite que de l'image : les vues, les zones, les forwarders et l'intégration ACME appartiennent au service, et ont le leur.

Ce que l'image contient vraiment

Plus intéressant que la liste des drapeaux : ce qui reste dans l'image finale.

bind9 est l'image la plus dépouillée du parc. Quatre exécutables seulement — le serveur, le validateur de configuration, le binaire d'entrée en Go, et le PID 1 — et, en dehors des fuseaux horaires, 33 chemins qui lui appartiennent en propre. Pour comparaison, la même image porte 624 fichiers de données de fuseaux horaires, soit dix-neuf fois plus de chemins que tout le reste réuni.

C'est aussi l'une des deux seules images du parc à ne porter aucun magasin de certificats : elle ne va rien chercher en HTTPS depuis le conteneur, donc les 239 certificats que portent six autres images n'auraient rien à y faire.

Enfin, le privilège réseau nécessaire pour écouter sur le port 53 est une capability posée sur le binaire au moment du build, pas un privilège accordé au conteneur. C'est l'une des deux seules capabilities de tout le parc.

Le healthcheck interroge vraiment le DNS

Le contrôle de santé n'est pas une ouverture de socket : c'est une requête DNS UDP réelle, envoyée en boucle locale.

// Requête CH TXT version.bind
query := []byte{
    0xBE, 0x9D, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00,
    0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n',
    0x04, 'b', 'i', 'n', 'd', 0x00,
    0x00, 0x10, 0x00, 0x03, // QTYPE=TXT, QCLASS=CH
}
// Accepte toute réponse valide (QR=1), même REFUSED

Deux choix méritent d'être expliqués. Le premier : un connect TCP réussit dès que le processus écoute, c'est-à-dire avant que les zones soient chargées. Une requête qui obtient une réponse prouve que le pipeline complet fonctionne.

Le second est contre-intuitif : le healthcheck accepte REFUSED comme une réponse saine. C'est volontaire. Un serveur correctement configuré refuse les requêtes hors de ses vues autorisées ; exiger un NOERROR reviendrait à exiger que le serveur soit ouvert. Ce qui est vérifié, c'est qu'un paquet DNS bien formé revient — pas son verdict.

Ce qui se décide au build, et qu'on ne peut plus changer après

Trois choix sont figés au moment de la compilation, ce qui les rend invisibles à qui lit seulement la configuration du service.

DNS-over-HTTPS est retiré à la compilation, pas désactivé dans un fichier : inutilisé en interne, il ne dort donc pas dans l'image en attendant qu'une directive le réveille.

La signature TSIG est compilée avec le nécessaire pour ACME — les mises à jour DNS de Let's Encrypt, avec une politique restreinte aux seuls enregistrements de défi. Le mécanisme complet est décrit dans l'obtention des certificats.

Le système de construction va changer, et c'est le point de vigilance à garder en tête. La branche actuelle utilise encore autoconf ; son successeur repose sur meson, qui installera Python dans l'image de construction. Pour une image dont tout l'intérêt est de ne porter que quatre exécutables, une montée de version qui tire un interpréteur complet dans la chaîne de build mérite d'être vue venir plutôt que subie.

Déploiement

Cette image tourne en conteneur Podman sur le routeur, pas dans le cluster. Le transfert, la pose du tag, les pièges du bind-mount et le contrôle de dérive sont décrits dans Mettre à jour les images VyOS avec Ansible.


Liens

Articles connexes