Set-OPS-Public/docs/dns-interne.md
Daniel Allaire cdac6d0ea5 doc dns : l'interne et le public, deux sites, avec les schemas des processus et des flux
docs/dns-interne.md couvre desormais le DNS public sur deux sites : vue d'ensemble, chemin
d'une requete interne, publication d'une zone, requete de l'Internet, NS d'une zone (cinq
schemas Mermaid), et les tableaux des flux, des declarations, des gardes et de la bascule.
README de serveur_powerdns et de serveur_dns_public a jour ; le wiki y renvoie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:16:11 -04:00

23 KiB

DNS : interne et public

Pour qui : le mainteneur du DNS, et l'exploitant d'un site qui publie les noms de ses locataires. Le nom du fichier date du temps où il ne couvrait que l'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.

Vue d'ensemble

Deux services distincts, qui ne se parlent pas :

  • la résolution interne — chaque écosystème résout ses propres noms (*.internal) et l'Internet pour ses machines. Rien n'en sort.
  • le DNS public — les zones publiques des locataires (chezlepro.ca, technolibre.ca), écrites chez le locataire, servies à l'Internet par deux sites, sous deux noms.
flowchart LR
  NET(("Internet"))
  subgraph LOC["Les locataires — vivent au site de Chezlepro"]
    direction TB
    P1["infra-dns-01 · OPS-Chezlepro · 10.17.19.11<br/>pdns@public :5300<br/>primaire caché, signe DNSSEC"]
    P2["infra-dns-01 · OPS-Technolibre · 10.23.19.11<br/>pdns@public :5300<br/>primaire caché, signe DNSSEC"]
  end
  subgraph SC["Site de Chezlepro"]
    direction TB
    BIF["bifrost-1<br/>WAN 69.70.26.62:53"]
    S1["site-dnspub-01 · 10.37.37.21<br/>dnsdist :1053 → PowerDNS :53<br/><b>dns1.chezlepro.ca</b>"]
    BIF -- "redirection :1053" --> S1
  end
  subgraph ST["Site de TechnoLibre"]
    direction TB
    REL["relais · VPS OVH<br/>51.79.45.193"]
    REQ["requin<br/>opt9 RELAIS · opt10 lien"]
    S2["site-dnspub-01 · 10.31.37.21<br/>dnsdist :1053 → PowerDNS :53<br/><b>dns1.technolibre.ca</b>"]
    REL -- "tunnel WireGuard" --> REQ
    REQ -- "redirection :1053" --> S2
  end
  P1 & P2 -- "NOTIFY · AXFR signé TSIG" --> S1
  P1 & P2 -. "NOTIFY · AXFR signé TSIG<br/>par le lien entre les sites" .-> S2
  NET -- "53" --> BIF
  NET -- "53" --> REL
Pièce Rôle Où Section
plancher /etc/hosts résout la flotte, DNS éteint chaque VM Trois couches
Unbound LE résolveur d'un écosystème infra-dns-01 (locataire), site-dns-01 (site) idem
PowerDNS principal autorité sur la zone .internal boucle locale, derrière Unbound idem
PowerDNS pdns@public primaire caché des zones publiques infra-dns-01 du locataire, :5300 Zones publiques
PowerDNS + dnsdist serveur de noms public du site, secondaire site-dnspub-01 de chaque site Deux sites

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.

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.

Le chemin d'une requête interne

flowchart TB
  Q["Une machine du locataire<br/>cherche un nom"] --> H{"/etc/hosts<br/>le plancher"}
  H -- "machine de la flotte" --> A1["adresse — sans DNS"]
  H -- "autre nom" --> U["Unbound · infra-dns-01:53<br/>serveur_resolveur"]
  U -- "*.internal et zone inverse<br/>stub-zone" --> PA["PowerDNS principal<br/>127.0.0.1:5300"]
  U -- "tout le reste<br/>récursion depuis la racine" --> NET(("Internet"))
  Q -. "locataire neuf, sans résolveur à lui" .-> SD["site-dns-01<br/>le résolveur du site<br/>serveur_resolveur_site"]
  • Le résolveur est la seule porte. Quand PowerDNS partage l'hôte d'Unbound, il écoute sur la boucle locale (5300) : personne ne l'interroge directement (port derive, roles/serveur_powerdns/meta/flux.yml).
  • Le site prête ses noms aux locataires neufs. Tant qu'un locataire n'a pas son résolveur, client_resolveur le fait interroger celui du site (serveur_resolveur_site, dont la liste d'autorisation se dérive des locataires hébergés), puis le bascule chez lui.
  • Où mène un nom exposé, vu de l'intérieur : resolution_interne dans plan/domaines.yml. service (le site) : la zone mène au service lui-même — l'edge est pour les personnes. edge (défaut, les locataires) : la machine passe par l'edge, parce que certains services n'existent en HTTPS que derrière lui (Keycloak).
  • Une machine qui naît interroge dns_amorcage (la patte de la frontière) tant que le résolveur de son site n'existe pas.

Groupes

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 :

exemple.internal

Elle est definie dans :

instance/inventories/<inventaire>/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 :

hote dans hotes_actifs
ansible_host defini

Exemple :

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 :

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.<zone>, 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é.

Le devis sonde aussi la colle de chaque serveur désigné — ceux du site et ceux des pairs (Deux sites).

Au 2026-10-14 : les deux zones sont servies par les deux sites, signées, au même serial ; aucune n'est basculée chez son registraire. chezlepro.ca reproduit la production (rien ne serait perdu). La décision pour technolibre.ca revient au responsable désigné de TechnoLibre.

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_<zone>). 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 <zone> 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.

Deux sites, deux serveurs de noms

Ajouté le 2026-10-14. Le registre des .ca exige deux serveurs de noms ; deux serveurs sur le même réseau tomberaient ensemble. Le second est au site de TechnoLibre, tenu par une autre organisation, sur un autre réseau.

Chaque site sert les zones publiques de tous les locataires qu'il connaît : les siens, et ceux de son pair. Les deux serveurs sont secondaires l'un comme l'autre, et tirent chacun la zone directement du primaire du locataire — jamais l'un de l'autre (disable-axfr=yes au site). Un site qui tombe n'emporte donc pas la copie de l'autre.

Site Serveur Nom public Adresse publique Chemin depuis l'Internet
Chezlepro site-dnspub-01 · 10.37.37.21 dns1.chezlepro.ca 69.70.26.62 WAN de bifrost-1 → :1053
TechnoLibre site-dnspub-01 · 10.31.37.21 dns1.technolibre.ca 51.79.45.193 relais (VPS) → tunnel opt9 de requin → :1053

Publier une zone

sequenceDiagram
  autonumber
  participant X as Exploitant du locataire
  participant P as pdns@public<br/>infra-dns-01 du locataire
  participant C as site-dnspub-01<br/>site de Chezlepro
  participant T as site-dnspub-01<br/>site de TechnoLibre
  X->>P: rôle serveur_powerdns — la zone est rendue depuis le plan
  Note over P: SOA et NS = dns1 du site + dns1 de chaque pair<br/>serial = suit le contenu · signée avec la clé de la voûte
  P-)C: NOTIFY (only-notify : les seuls serveurs du contrat)
  P-)T: NOTIFY, par le lien entre les sites
  C->>P: SOA ? (UDP 5300)
  C->>P: AXFR signé TSIG (TCP 5300)
  P-->>C: zone déjà signée — servie telle quelle (PRESIGNED)
  T->>P: SOA ? puis AXFR signé TSIG, par le lien
  P-->>T: zone déjà signée
  • Le locataire écrit, les sites servent. Les deux sites ne détiennent ni la zone en source, ni la clé DNSSEC.
  • only-notify et also-notify bornent les notifications aux adresses du contrat du site (dns_public_site et dns_public_pairs). Sans eux, le primaire notifierait les adresses publiques de ses NS, depuis l'intérieur du locataire.
  • Le transfert passe par le lien quand le secondaire est sur l'autre site : la frontière du site où vit le locataire laisse entrer le transfert et sortir les NOTIFY ; celle du site qui réplique pose une route /32 vers chaque primaire. Le devis les dérive de la fiche que porte chaque locataire (où il vit), et P24 juge ces routes à part.

Une requête de l'Internet

sequenceDiagram
  participant R as Résolveur sur Internet
  participant F as Frontière<br/>bifrost-1 ou requin
  participant D as dnsdist :1053
  participant A as PowerDNS :53
  R->>F: chezlepro.ca MX ? (port 53)
  Note over F: TechnoLibre : arrive par le relais et le tunnel opt9,<br/>et repart par lui (reply-to RELAIS_GW)
  F->>D: redirection vers :1053
  Note over D: refuse NOTIFY, UPDATE, AXFR · tronque ANY en UDP<br/>débit borné par /24
  D->>A: requête admise
  A-->>D: réponse signée
  D-->>R: réponse

Sans reply-to, la réponse du site de TechnoLibre se perdrait : elle repartirait par la route par défaut, le WAN, qui n'a qu'une adresse privée en amont (mesuré le 2026-10-13).

Les flux

Flux De Vers Port Chemin
Requête publique Internet dnsdist du site 53 → 1053, UDP et TCP redirection de la frontière ; par le relais au site de TechnoLibre
Requête admise dnsdist PowerDNS du site 53, boucle de l'hôte local
NOTIFY pdns@public du locataire PowerDNS de chaque site 53/UDP local ; par le lien vers l'autre site
SOA, puis AXFR TSIG PowerDNS de chaque site pdns@public du locataire 5300, UDP puis TCP local ; par le lien depuis l'autre site
Résolution interne machine du locataire Unbound de son écosystème 53 dans l'écosystème
Zone .internal Unbound PowerDNS principal 5300, boucle locale local
Récursion Unbound Internet 53 sortie de la frontière

Ces flux sont déclarés par les rôles (meta/flux.yml de serveur_dns_public, serveur_powerdns, serveur_resolveur) et par le lien (opnsense_liens_sites) ; le devis de la frontière et le pare-feu Proxmox en dérivent leurs règles. Aucun n'est écrit à la main sur une frontière.

Ce qui se déclare, et où

Clé Fichier Ce qu'elle décide
autorite: primaire-cache, dnssec, enregistrements plan/domaines.yml du locataire la zone est publique, signée, et ce qu'elle porte
dns_public_nom plan/10-intrants.yml du site le nom de SON serveur de noms (dns1.<domaine de l'organisation>)
dns_public_pairs plan du site, des deux côtés le site pair qui réplique ; inactif n'émet rien
opnsense_liens_sites opnsense.yml du site le tunnel que prennent NOTIFY et transferts
opnsense_wan_ip opnsense.yml du site l'adresse publique de son serveur de noms

Le contrat du site (make site-intrants) en tire, pour chaque locataire, ce qu'il dépose dans sa fiche (fiche-site.yml) : dns_public_site, dns_public_pairs, dns_public_nom et dns_public_noms_pairs (le nom et l'adresse publique du serveur de chaque pair, lus dans son plan et sa carte, par les mêmes fonctions que pour le site lui-même).

Les NS d'une zone

flowchart LR
  PC["plan du site de Chezlepro<br/>dns_public_nom: dns1.chezlepro.ca"] --> K["contrat du site<br/>fiche du locataire"]
  PT["plan du site de TechnoLibre<br/>dns_public_nom: dns1.technolibre.ca<br/>+ opnsense_wan_ip"] -- "dns_public_noms_pairs" --> K
  K --> Z["zone-publique.db.j2<br/>chez le locataire"]
  Z --> NS["chezlepro.ca — NS dns1.chezlepro.ca, NS dns1.technolibre.ca<br/>A de dns1.chezlepro.ca : seulement dans chezlepro.ca<br/>A de dns1.technolibre.ca : seulement dans technolibre.ca"]
  • Un site, un nom : dns1 sous le domaine de l'organisation qui tient le site. Pas ns1/ns2, qui existent en production à d'autres adresses.
  • Seule la zone qui contient un nom en porte le A, à l'adresse publique de son site. Les autres zones ne le désignent que par son nom.
  • Sans pair, le rendu est identique octet pour octet à celui d'un site seul.

Les gardes

Garde Où Ce qu'elle refuse
relation des deux côtés P82 (prouver._dns_paires) un pair déclaré d'un seul côté — le second serveur serait vide sans que personne le voie
un pair nomme son serveur P82 (prouver._dns_paires) un pair actif sans dns_public_nom — ses zones n'en désigneraient qu'un, alors qu'il les sert
signé avant exposé P82 exposer un serveur public dont une zone n'est pas signée
nom et adresse du pair serveur_powerdns (zones-publiques.yml) un nom mal formé, une adresse privée, un nom égal au nôtre, un nom redéclaré
contrat déposé make site-intrants-verifier une fiche de locataire qui diffère du contrat (une adresse faussée la rend NON CONFORME)
routes du lien P24 une route /32 qu'aucune règle du lien n'utilise
zone servie nue sonde zones-publiques une zone signée servie sans signature ; signatures sous 6 jours (avertit), sous 3 (alerte)

Basculer chez un registraire

L'ordre, pour chaque domaine :

  1. make dns-bascule-devis — rien ne serait perdu, et chaque serveur désigné résout déjà publiquement.
  2. La colle chez le registraire du domaine qui contient le nom : dns1.chezlepro.ca chez celui de chezlepro.ca, dns1.technolibre.ca chez celui de technolibre.ca.
  3. Les deux NS chez le registraire de la zone basculée. Zone signée, sans DS : le domaine reste « non sécurisé », pas cassé.
  4. Vérifier depuis l'Internet, sur les deux adresses.
  5. Puis seulement le DS (make dnssec-ds). Jamais l'inverse.

État au 2026-10-14 : les deux sites servent chezlepro.ca et technolibre.ca au même serial, chacune avec deux NS signés. Ni la colle ni les NS ne sont encore chez les registraires.