Set-OPS-Public/docs/dns-interne.md
Daniel Allaire 788c5073dc DNS public, phase 1 : le locataire ecrit, le site sert
Role serveur_dns_public (secondaire public, transfert signe TSIG, aucune zone interne) ;
serveur_powerdns exerce enfin autorite primaire-cache dans une instance pdns@public a part,
pour que le site ne puisse jamais interroger la zone .internal. Relations derivees des plans
des locataires, mots de flux dns_public_site et primaires_dns_locataires. Eprouve avant
d ecrire : allow-axfr-ips et TSIG sont alternatifs, le primaire notifie aussi ses NS, le
serial fige aurait gele le secondaire. P82 refuse l exposition sans DNSSEC.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:08:20 -04:00

173 lines
7 KiB
Markdown

# 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/<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 :
```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.