# DNS interne > **Pour qui :** le **mainteneur** du DNS interne. Le service DNS interne est la premiere capacite de plateforme. > **Plancher de resolution independant du DNS.** Le socle (`hosts_statiques`, dans > `serveur_debian`) genere `/etc/hosts` sur **chaque** VM depuis l'inventaire : tout > l'ecosysteme se resout par nom **meme serveur DNS eteint** (et au bootstrap, avant > que PowerDNS ne soit la). PowerDNS devient une **commodite** (zone, externe, > dynamique), plus un point de defaillance. ## Trois couches, et une seule est optionnelle > **Ce paragraphe decrivait le modele d'avant le 2026-08-24** — un Unbound *sur chaque VM*, > en opt-in. Ce n'est plus le cas : `client_resolveur` **n'installe plus rien**, et son > integration est **universelle**, pas elective. ```text 1. hosts_statiques le PLANCHER : /etc/hosts genere sur chaque VM depuis l'inventaire -> l'ecosysteme se resout DNS eteint. Jamais optionnel. 2. serveur_powerdns l'AUTORITATIF de la zone souveraine (un par ecosysteme) 3. serveur_resolveur le RECURSIF : UN seul Unbound pour tout le tenant, qui recurse depuis la racine et delegue la zone souveraine a PowerDNS client_resolveur l'integration : ecrit /etc/resolv.conf pour designer ce resolveur ``` **`client_resolveur` n'installe plus de demon** (2026-08-24). Il en posait un par VM — N demons identiques de ~21 Mo pour quelques centaines de requetes. Il ne fait plus qu'une chose : **ecrire `/etc/resolv.conf`**. Son integration est marquee `universelle: true`, et elle n'a **aucune exemption, pas meme l'hote qui porte le resolveur** : il se sert lui-meme. L'ancien nom (`client_unbound`) mentait des lors qu'il n'installait plus Unbound. **L'integration suit l'existence du service, elle ne se declare pas.** Aucun `serveur_resolveur` au plan rend `client_resolveur_actif` faux et le role ne touche a rien : l'ecosysteme garde la resolution d'amorcage de cloud-init. C'est un choix valide, pas une panne. ## Groupes ```text serveur_powerdns -> autoritatif interne (PowerDNS Authoritative) serveur_resolveur -> LE recursif du tenant (Unbound), un seul client_resolveur -> integration universelle : designe ce resolveur dans /etc/resolv.conf ``` ## Zone initiale La zone initiale est : ```text exemple.internal ``` Elle est definie dans : ```text instance/inventories//group_vars/serveur_powerdns.yml ``` ## Enregistrement automatique Le role `serveur_powerdns` genere la zone a partir de l'inventaire. Les hotes qui remplissent ces conditions sont ajoutes automatiquement : ```text hote dans hotes_actifs ansible_host defini ``` Exemple : ```text web-frontal-01 ansible_host=10.0.2.31 -> web-frontal-01.exemple.internal A 10.0.2.31 ``` Les enregistrements additionnels explicites sont dans `serveur_powerdns_records`. ## Backend PowerDNS Le premier jalon utilise le backend BIND de PowerDNS. Raison : - pas de dependance prematuree a PostgreSQL ; - zone lisible et generee par Ansible ; - idempotence simple ; - bon point de depart pour la supervision. Quand `serveur_postgresql` sera stable, il sera possible de migrer vers un backend SQL si le besoin operationnel le justifie. ## La bascule de `/etc/resolv.conf` est protegee Le role `client_resolveur` ne bascule `/etc/resolv.conf` qu'apres avoir verifie que le resolveur repond **deja** — pour l'interne *et* pour l'Internet : ```yaml client_resolveur_apply: true client_resolveur_confirm: true ``` Sans ces deux variables, le role prépare Unbound mais ne modifie pas `/etc/resolv.conf`. Cette protection est volontaire : une mauvaise configuration DNS peut couper la resolution de noms. C'est la dependance la plus dangereuse du lot — basculer un hote sur un resolveur qui n'est pas encore pret le rend **muet**, et le runner qui devrait reparer tombe avec les autres. Le playbook de groupe applique donc l'hote qui *porte* le resolveur avant ceux qui s'y adressent, et la preuve **P44** refuse tout ecart entre cette declaration et lui. ## Le piege qui a coute deux jours : la racine signee nie notre TLD `internal.` n'est **pas delegue dans la racine**, qui est signee : elle rend donc une preuve NXDOMAIN *validee* pour ce TLD. Or `harden-below-nxdomain` — **actif par defaut** dans Unbound — tient ce « non » pour prouve et repond NXDOMAIN pour **tout** nom sous `internal.` depuis son cache, **sans jamais interroger la `stub-zone`** declaree plus bas. La delegation etait correcte. L'autoritatif repondait juste. Pas une requete ne lui parvenait. Ce qui declenche l'empoisonnement : n'importe quelle question sur un nom inexistant sous `internal.` — y compris la zone d'**un autre ecosysteme**, que ce resolveur ne sert pas et va donc chercher a la racine. Sur un resolveur partage, ca arrive en permanence. Et la panne parait **intermittente** : au redemarrage le cache est vide, tout fonctionne, on conclut que c'est regle. Le remede mesure (2026-09-02) est `harden-below-nxdomain: no` dans `roles/serveur_resolveur/templates/setops.conf.j2` — **pas** `aggressive-nsec`, qui traite un autre symptome. ## Surveillance a prevoir Les premiers checks utiles : - service `pdns` actif ; - port UDP/TCP 53 disponible ; - SOA de `exemple.internal` resoluble ; - enregistrement `ns1.exemple.internal` resoluble ; - enregistrements des hotes actifs presents ; - serial de zone attendu ; - latence de resolution. ## Zones publiques > Ajouté le 2026-09-16. Le mode `primaire-cache` était **validé par le schéma et consommé par > rien** depuis sa création — *une autorité qu'on s'attribue sans l'exercer est une panne > différée*. `plan/domaines.yml` porte un champ `autorite` par domaine. Deux valeurs sont exercées : | `autorite` | Ce que ça veut dire | Qui sert la zone | |---|---|---| | `auto-heberge` | zone **interne** (`.internal`), servie à l'écosystème seul | l'instance principale de `serveur_powerdns`, sur la boucle locale | | `primaire-cache` | zone **publique** : le locataire l'écrit, le site la sert | `pdns@public` chez le locataire (primaire caché), `serveur_dns_public` au site (secondaire public) | `delegue` est accepté par le validateur et **n'est consommé par aucun rôle**. Il ne faut pas le déclarer en croyant qu'il fait quelque chose. **Le locataire écrit, le site sert, un site pair réplique.** Le primaire reste chez le locataire : quand il part, il emporte sa zone. Le détail — et ce que l'épreuve de PowerDNS a appris — est dans `roles/serveur_dns_public/README.md`. Trois règles, chacune payée ou mesurée : - **TSIG seul ouvre le transfert.** `allow-axfr-ips` et TSIG sont *alternatifs* : une adresse listée obtient la zone sans signature. L'instance publique n'autorise que la boucle locale. - **Une instance à part pour le public.** Faire écouter l'instance principale sur l'adresse de l'hôte aurait permis au serveur public du site d'interroger la zone `.internal`. - **Le serial suit le contenu.** Un serial figé ferait garder au secondaire l'ancienne zone pour toujours. **Rien n'est exposé à Internet en phase 1.** P82 refuse d'exposer le serveur public tant que toutes ses zones ne sont pas signées DNSSEC. ### Ce qu'une zone publique porte > Ajouté le 2026-09-16, phase 2. Une zone qui ne portait que ses expositions web aurait coupé > le courriel de production à la minute où le registraire l'aurait suivie. Chaque zone `primaire-cache` déclare ses **enregistrements** au plan (`enregistrements:` dans `plan/domaines.yml`) : A, AAAA, CNAME, MX, TXT, CAA. `valider_domaines` refuse une adresse privée, une cible incomplète, un CNAME à l'apex ou à côté d'un autre type, un MX sans priorité, un TXT avec guillemets ou hors ASCII, un nom écrit en absolu (`mx.chezlepro.ca` sous `chezlepro.ca`), et tout enregistrement dans une zone que Set-OPS n'écrit pas. Le **SOA et les NS** ne se déclarent pas : ils désignent le serveur de noms du site, sous le nom que **le site** déclare (`dns_public_nom`, contrat du site, reçu par chaque locataire). Seule la zone qui contient ce nom porte son A. Le premier choix, `ns1.`, aurait déplacé en silence un `ns1.chezlepro.ca` qui existe en production vers une autre adresse. **Avant de basculer : `make dns-bascule-devis`.** Il compare le plan au DNS en service, nom par nom et type par type, sur les noms déclarés et une liste de sondes usuelles. Il rend ce qui serait **perdu**, **changé**, **ajouté**, ou **abandonné en connaissance de cause** (les marques `heritage=external-dns`), et les préalables : le serveur de noms doit déjà résoudre publiquement, et il en faut **deux** (exigence du registre `.ca`). Une mesure qui échoue rend « mesure impossible », jamais « absent ». Sa limite est écrite à chaque rapport : sans transfert de zone, un export chez le fournisseur actuel reste la seule preuve d'exhaustivité. Au 2026-09-16 : `chezlepro.ca` reproduit la production (rien ne serait perdu) ; `technolibre.ca` est **préparée, pas basculée** — la décision revient au responsable désigné de TechnoLibre, et elle attend que `dns1.chezlepro.ca` soit publié. ### Signature DNSSEC : le locataire signe, avec la clé de sa voûte > Ajouté le 2026-09-16, phase 2. Éprouvé sur les machines réelles avant d'être codé. `dnssec: true` sur une zone `primaire-cache` la fait signer **par le primaire du locataire**, avec **une** clé CSK ECDSA P-256 tenue dans **sa** voûte (`vault_dnssec_`). Le site ne détient aucune clé : il reçoit la zone déjà signée et la sert telle quelle (PowerDNS pose `PRESIGNED` au transfert). | Geste | Commande | Écrit ? | |---|---|---| | Créer la clé d'une zone | `python3 scripts/dnssec.py generer ` | la voûte du locataire (copie de sûreté chiffrée, relue avant et après) | | Lire le DS à remettre au registraire | `make dnssec-ds` | rien — calculé depuis la voûte, sans la machine | | Voûte, plan et registre concordent-ils ? | `make dnssec-verifier` | rien | Pourquoi la clé ne naît pas sur la machine : une reconstruction depuis zéro signerait avec une **autre** clé, le DS du registraire ne correspondrait plus, et le domaine deviendrait **BOGUS** pour tout résolveur validant, sans qu'aucune machine soit en panne. Trois règles, chacune mesurée : - **Changer la clé ou retirer la signature pendant qu'un DS est publié est refusé** par l'outil de la machine (`setops-dnssec-zone`) comme par `dnssec.py`. Une mesure du DS qui échoue vaut refus. - **Le serial servi est l'heure (`SOA-EDIT EPOCH`).** PowerDNS renouvelle ses signatures chaque semaine (inception au début de la semaine précédente, expiration deux semaines après le début de la semaine courante). `INCEPTION-EPOCH`, essayé d'abord, ne dépassait notre serial qu'au moment où les signatures du site expiraient. Avec l'heure, le site retire la zone à chaque rafraîchissement ; les signatures servies gardent 7 à 14 jours. - **L'état DNSSEC fait partie du corps de la zone.** Signer ne change aucun enregistrement : sans la ligne `; DNSSEC : …`, le serial n'aurait pas bougé et le site aurait gardé la version non signée. La sonde `zones-publiques` du site alerte sur une zone signée servie **nue**, avertit sous 6 jours de signature restante, et alerte sous 3. **Ordre de la mise en service** : basculer les serveurs de noms (zone signée, sans DS : le domaine reste « non sécurisé », pas cassé), vérifier, **puis** remettre le DS au registraire. Jamais l'inverse.