diff --git a/CHANGELOG.md b/CHANGELOG.md index 26da73c..d9d7316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,70 @@ # CHANGELOG — Set-OPS +## 2026-08-24 — Un résolveur par tenant, et non plus un par machine + +`client_unbound` posait un Unbound sur **chaque VM**. C'était étanche, et c'était N démons +identiques pour un service unique. + +**Mesure prise avant de décider** : ~21 Mo de RSS par VM, et **28 à 189 requêtes** servies +depuis le démarrage. Le gain de cache mutualisé est donc négligeable ; ce qu'on récupère, +c'est un démon au lieu de cinq — et **un endroit à regarder** au lieu de cinq. + +### Pourquoi pas sur la frontière + +C'était la proposition, et elle aurait été la plus économe. Mais un résolveur partagé par +tout le SITE devrait connaître la zone interne de **chaque tenant** : sans vues par réseau +soigneusement réglées, le tenant A résoudrait les noms du tenant B. Et un tenant dont le +résolveur vit chez l'hébergeur **ne peut plus s'émanciper avec**. + +La récursion est générique ; **la zone interne ne l'est pas**. C'est ce qui fait du +résolveur un service de tenant, et non de fabric. + +### La cohabitation avec l'autoritatif + +Les deux vivent sur `infra-dns-01` et voudraient le port 53. Le partage est **dérivé, pas +déclaré** — l'exploitant n'a pas à se souvenir de déplacer un port parce qu'il a ajouté un +rôle : + +``` +résolveur (Unbound) 10.29.19.11:53 + 127.0.0.1:53 la seule porte +autoritatif (PowerDNS) 127.0.0.1:5300 derrière lui +``` + +Le port de PowerDNS est déclaré `derive` dans son `meta/flux.yml` — `verifier_ports` traite +un port non numérique comme « pas une écoute fixe », ce qui est exactement le cas : les +deux lient le port 53, mais sur des **adresses** différentes. + +### Deux pièges, dont un que le harnais seul a vu + +**Unbound refuse d'interroger une loopback.** `do-not-query-localhost` vaut `yes` d'origine +— une protection contre les boucles. Or l'autoritatif vit désormais sur `127.0.0.1:5300`, +derrière le résolveur. Sans la lever, **toute la zone souveraine rendait SERVFAIL**. + +**Et la panne était masquée par le plancher.** `getent hosts forge.genese.internal` +répondait `10.29.16.11` sur les cinq machines — c'était `/etc/hosts`, pas le DNS. J'ai +d'abord conclu que la résolution fonctionnait. Seul un `dig` explicite a montré le +SERVFAIL. Le plancher fait son travail — mais il rend une panne DNS invisible à qui +mesure avec le mauvais instrument. + +C'est la tâche de validation du rôle qui a refusé de se dire satisfaite, et elle avait +raison contre moi. + +### Le client sait se retirer + +`client_resolveur` **désinstalle** l'Unbound local après avoir basculé — jamais avant, +sinon l'hôte perdrait toute résolution au milieu du play. L'hôte qui porte le résolveur est +épargné : c'est le même paquet. + +Sans cette tâche, le rôle aurait changé le `resolv.conf` sans rien économiser, et la raison +même du changement aurait été perdue. + +### Le renommage + +`client_unbound` n'installant plus Unbound, son nom mentait. Il devient `client_resolveur` +— une vingtaine de fichiers, quatre plans d'instance et six modèles. Le harnais a rattrapé +chaque oubli : playbook homonyme manquant, inventaires en écart, plan de recette périmé, +README absent. + ## 2026-08-24 — Collabora en natif : la dernière exception conteneurisée tombe `serveur_collabora` lançait `collabora/code` dans Docker — **seule exception** de la diff --git a/docs/audit/plan-de-recette.md b/docs/audit/plan-de-recette.md index 9b9ecb9..4fe2aee 100644 --- a/docs/audit/plan-de-recette.md +++ b/docs/audit/plan-de-recette.md @@ -68,7 +68,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c | 1 | Le plancher, sans DNS | Sur un nœud : « commande » | 👁 observe | — | | 2 | Interroge l'autoritatif | Demande à PowerDNS directement : « commande » | 👁 observe | — | | 3 | Vois les couches | Compare getent hosts (plancher) et dig (DNS) : deux chemins, même IP. | 👁 observe | — | -| 4 | Casse & répare | Sur un nœud sans client_unbound, vide /etc/hosts de ses entrées chezlepro (garde une sauvegarde !) et coupe l'accès au DNS : la résolution interne échoue. Restaure /etc/hosts : ça remarche sans DNS. Tu viens de *sentir* pourquoi le planc… | 🔨 casse-répare | — | +| 4 | Casse & répare | Sur un nœud sans client_resolveur, vide /etc/hosts de ses entrées chezlepro (garde une sauvegarde !) et coupe l'accès au DNS : la résolution interne échoue. Restaure /etc/hosts : ça remarche sans DNS. Tu viens de *sentir* pourquoi le pla… | 🔨 casse-répare | — | *Source : [DNS & résolution de noms](DNS-et-résolution) · § À toi de jouer.* diff --git a/docs/audit/preuve-2026-08-24.md b/docs/audit/preuve-2026-08-24.md index 91fdbb3..9298be1 100644 --- a/docs/audit/preuve-2026-08-24.md +++ b/docs/audit/preuve-2026-08-24.md @@ -20,15 +20,15 @@ | P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | | P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | | P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). | -| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 34 groupes classes, aucun cycle, aucune arete en arriere. | -| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 32 rôles, 83 flux, schéma + matrice OK. | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 35 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 33 rôles, 84 flux, schéma + matrice OK. | | P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | | P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml | | P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | | P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. | | P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. | | P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | -| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 15 hotes, 32 groupes (inventaire dechiffre et parse). | +| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 15 hotes, 33 groupes (inventaire dechiffre et parse). | | P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. | | P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. Voute reelle : 25 cle(s), aucun manque. | | P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | @@ -36,21 +36,21 @@ | P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. | | P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). | | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 7 reseau(x), aucune collision avec la plage tenant. | -| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 55 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,10.29.19.41/32,192.168.254.2/32,192.168.255.2/32. | -| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 48 groupe(s), 77 regle(s). | -| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 15 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 52 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,10.29.19.41/32,192.168.254.2/32,192.168.255.2/32. | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 51 groupe(s), 83 regle(s). | +| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 15 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). | | P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. | -| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 26 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 15, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 27 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 16, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | -| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 47 scripts expliques et atteignables, 98 cibles make documentees, 58 roles avec README. | -| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 34 exigence(s) de role, toutes satisfaites (121 cle(s) declaree(s) par l'instance). | -| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (34 groupes). | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 47 scripts expliques et atteignables, 98 cibles make documentees, 59 roles avec README. | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 36 exigence(s) de role, toutes satisfaites (121 cle(s) declaree(s) par l'instance). | +| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (22 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | | P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. | -| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 33 role(s) serveur/client tous nommes, 34 groupe(s) cite(s) en table existent tous. | +| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 34 role(s) serveur/client tous nommes, 35 groupe(s) cite(s) en table existent tous. | | P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | | P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | | P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 43 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index 5c708f9..cc99b22 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -75,7 +75,7 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis - ✅ **soldé** — README de rôles : **tous les rôles en ont un** (les 12 manquants écrits le 2026-07-29 : `serveur_debian`, `hosts_statiques`, `resoudre_base`, `resoudre_annuaire`, `serveur_dovecot`, `serveur_postfix`, `serveur_rspamd`, `client_backup`, `serveur_backup`, - `client_unbound`, `serveur_oauth2_proxy`, `serveur_icingaweb2`). + `client_resolveur`, `serveur_oauth2_proxy`, `serveur_icingaweb2`). - ✅ **soldé** — `expose` **est** consommé au déploiement : `plan/applications.yml` → filtre `expositions_des_applications` → vhosts nginx dérivés (`roles/serveur_nginx/tasks/main.yml`, template `expositions.conf.j2`, drapeau diff --git a/docs/catalogue-services.md b/docs/catalogue-services.md index 88fb49c..ba9fdc7 100644 --- a/docs/catalogue-services.md +++ b/docs/catalogue-services.md @@ -37,7 +37,7 @@ Ce que la reconstruction couvre, par capacité : |---|---|---| | Socle et durcissement | `serveur_debian`, `serveur_durci` | clone du gabarit doré → machine conforme | | Confiance | `serveur_step_ca`, `client_pki` | mTLS avec SAN dérivés du plan, renouvellement | -| Noms | `serveur_powerdns`, `client_unbound` | autoritaire interne + résolveur local | +| Noms | `serveur_powerdns`, `client_resolveur` | autoritaire interne + résolveur local | | Identité | `serveur_openldap`, `serveur_keycloak` | LDAPS, SSO OIDC, **fédération LDAP automatisée** (`tasks/federation-ldap.yml`), exposé par le plan (`auth.`) | | Passerelle SSO | `serveur_oauth2_proxy` | SSO devant une app sans OIDC natif (éprouvé sur Icinga Web 2) | | Données | `serveur_postgresql`, `serveur_redis` | bases et comptes dérivés du registre, TLS `verify-full` | @@ -52,10 +52,11 @@ Ce que la reconstruction couvre, par capacité : | Exploitation | `serveur_ops` | le poste depuis lequel l'ecosysteme se reconstruit : Ansible epingle, genome clone depuis **sa propre forge**, cle SSH propre — **sans** la voute ni son mot de passe | | Source d'artefacts | `serveur_artefacts`, `client_artefacts` | cache apt de l'ecosysteme (apt-cacher-ng) : les paquets viennent de chez soi, pas de six serveurs etrangers — **mode hors ligne** pour prouver ce que le cache detient vraiment | | Runner de site | `serveur_ops_site` | le pouvoir de **materialiser** : creer et detruire des VM sur la fabric. Detient la voute du SITE, chiffree, et n'entre JAMAIS chez un tenant — reserve a l'ecosysteme de l'hebergeur | +| Resolution | `serveur_resolveur`, `client_resolveur` | UN resolveur recursif par tenant (Unbound), qui recurse depuis la racine et delegue la zone souveraine a PowerDNS. Remplace les N demons locaux d'avant le 2026-08-24 | | Agents de flotte | `client_metrique`, `client_journal`, `client_smtp` | collecte et relais sur toute la flotte | *Rôles retirés (2026-07-04, supersédés ou hors conception)* : `serveur_sendmail` -(→ Postfix), `client_dns` (→ plancher `/etc/hosts` + `client_unbound`), `client_ldap` +(→ Postfix), `client_dns` (→ plancher `/etc/hosts` + `client_resolveur`), `client_ldap` (login LDAP au niveau OS, hors design). > **Ce que « éprouvé » ne dit pas.** La reconstruction prouve que le moteur mène une @@ -154,7 +155,7 @@ boîtes (Dovecot). La coupure suit l'exposition, pas le logiciel. | Confiance PKI / ACME | `client_pki` | **tout hôte** (universelle) | | Métriques Prometheus | `client_metrique` | **tout hôte** (universelle) | | Journaux vers Loki | `client_journal` | **tout hôte** (universelle) | -| Résolution locale (Unbound) | `client_unbound` | **tout hôte** (universelle) | +| Résolution locale (Unbound) | `client_resolveur` | **tout hôte** (universelle) | | Relais SMTP | `client_smtp` | déclaré par hôte, dans le plan | | Sauvegarde restic | `client_backup` | déclaré par hôte — **obligatoire pour tout détenteur d'état** (P36) | @@ -271,7 +272,7 @@ L'ordre ci-dessous privilégie les dépendances structurantes avant les applicat - Tout service exposé en HTTP(S) doit prévoir son intégration avec `serveur_nginx`. - Tout service avec authentification humaine doit prévoir son intégration avec `serveur_keycloak`, sauf justification contraire. - Tout service générant des alertes ou notifications doit prévoir `client_smtp`. -- Toute VM de service rejoint `client_pki`, `client_metrique`, `client_journal` et `client_unbound` — **par dérivation, sans rien écrire** (intégrations universelles, P26). La supervision, elle, ne pose rien sur l'hôte. +- Toute VM de service rejoint `client_pki`, `client_metrique`, `client_journal` et `client_resolveur` — **par dérivation, sans rien écrire** (intégrations universelles, P26). La supervision, elle, ne pose rien sur l'hôte. - Tout hôte qui **détient de l'état** doit porter `client_backup` (P36 le refuse sinon). - Tout service utilisant un certificat interne doit dépendre de `client_pki`. - Tout rôle serveur doit documenter ses ports, secrets, sauvegardes, dépendances et groupes clients associés. diff --git a/docs/couches-deploiement.yml b/docs/couches-deploiement.yml index 98a62bc..65fc764 100644 --- a/docs/couches-deploiement.yml +++ b/docs/couches-deploiement.yml @@ -39,6 +39,9 @@ couches: - serveur_postgresql - serveur_openldap - serveur_powerdns + # Le resolveur du tenant vient APRES son autoritatif : il le prend en stub-zone, + # et sa validation exige que la zone souveraine reponde deja. + - serveur_resolveur - serveur_redis - serveur_prometheus - serveur_loki @@ -78,5 +81,5 @@ couches: - client_journal - client_smtp - client_backup - - client_unbound + - client_resolveur - client_artefacts diff --git a/docs/dependances-groupes.yml b/docs/dependances-groupes.yml index 60d11f8..14c8fa2 100644 --- a/docs/dependances-groupes.yml +++ b/docs/dependances-groupes.yml @@ -127,6 +127,14 @@ groupes: raison: "Le runner de site n'ajoute qu'un pouvoir a un poste d'exploitation existant : sans lui, rien a quoi l'attacher." surveillance: "Verifier que la voute du site est presente ET CHIFFREE, et que la carte de la fabric est lisible." + serveur_resolveur: + requiert_groupes_actifs: + - serveur_powerdns + # Le resolveur prend la zone souveraine en STUB-ZONE : sans autoritatif, il ne saurait + # resoudre aucun nom de l'ecosysteme, et sa propre validation echouerait. + raison: "Le resolveur du tenant delegue la zone souveraine a l'autoritatif ; sans lui, il ne sait rien de l'ecosysteme." + surveillance: "Verifier qu'il repond pour la zone interne ET pour un nom de l'Internet." + serveur_nextcloud: requiert_groupes_actifs: - serveur_postgresql diff --git a/docs/dns-interne.md b/docs/dns-interne.md index 26186ff..e0409dc 100644 --- a/docs/dns-interne.md +++ b/docs/dns-interne.md @@ -8,17 +8,17 @@ Le service DNS interne est la premiere capacite de plateforme. > `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. Le resolveur local `client_unbound` est +> dynamique), plus un point de defaillance. Le resolveur local `client_resolveur` est > **optionnel** (opt-in, avec bascule validee) : sans lui, le plancher `/etc/hosts` suffit. ## Groupes ```text serveur_powerdns -> service DNS central PowerDNS Authoritative -client_unbound -> resolveur local optionnel (opt-in, bascule validee) +client_resolveur -> resolveur local optionnel (opt-in, bascule validee) ``` -`client_unbound` (résolveur local optionnel) peut viser PowerDNS en stub-zone + récursion. +`client_resolveur` (résolveur local optionnel) peut viser PowerDNS en stub-zone + récursion. ## Zone initiale @@ -69,15 +69,15 @@ Quand `serveur_postgresql` sera stable, il sera possible de migrer vers un backe ## Résolveur local (optionnel) -Le role `client_unbound` (opt-in) installe un résolveur récursif local qui, en stub-zone, +Le role `client_resolveur` (opt-in) installe un résolveur récursif local qui, en stub-zone, délègue les noms internes à PowerDNS et récurse le reste. La bascule du resolver local est **protegee** (le role valide qu'Unbound répond AVANT de basculer `/etc/resolv.conf`) : ```yaml -client_unbound_apply: true -client_unbound_confirm: true +client_resolveur_apply: true +client_resolveur_confirm: true ``` Sans ces deux variables, le role prépare Unbound mais ne modifie pas `/etc/resolv.conf`. diff --git a/docs/frontiere-opnsense.md b/docs/frontiere-opnsense.md index f2902f1..39f57c4 100644 --- a/docs/frontiere-opnsense.md +++ b/docs/frontiere-opnsense.md @@ -420,7 +420,7 @@ Reste à trancher sur une interface **physique** : `ip ?`, `access-group ?`, `sp et `` ; rien dans le modèle ne peut les deviner. - ~~**La sortie générale** n'est pas déclarée~~ — **réglé le 2026-08-02.** Elle est déclarée dans le registre, donc dérivée comme le reste : `serveur_debian` (le socle, porté - par tous les hôtes) déclare 443, 80 et 123/udp — dépôts apt et horloge ; `client_unbound` + par tous les hôtes) déclare 443, 80 et 123/udp — dépôts apt et horloge ; `client_resolveur` déclare 53 en UDP et TCP, la récursion depuis la racine que le choix souverain implique. Le `block out` de la section 5 est donc un vrai default-deny **assumé**, et le devis l'énonce désormais au lieu de le poser en silence. diff --git a/docs/integrations-vm.md b/docs/integrations-vm.md index 9b377e5..f971b9a 100644 --- a/docs/integrations-vm.md +++ b/docs/integrations-vm.md @@ -18,7 +18,7 @@ integration: sauf_role: serveur_step_ca # facultatif — voir « exemptions » ``` -**Facultative** — `client_backup`, `client_smtp`, `client_unbound`. Là il y a un vrai choix, +**Facultative** — `client_backup`, `client_smtp`, `client_resolveur`. Là il y a un vrai choix, et il se déclare par serveur, dans `plan/serveurs.yml : integrations`. **Pourquoi cette inversion.** Le plan portait 57 lignes d'intégration écrites à la main. 28 diff --git a/docs/intrants-communs.md b/docs/intrants-communs.md index 270c536..d8f4e5b 100644 --- a/docs/intrants-communs.md +++ b/docs/intrants-communs.md @@ -40,7 +40,7 @@ nftables baseline · fail2ban SSH · auditd · AppArmor · sysctl · unattended- journald (rétention) · core_dumps · systemd_ssh_auto. ### F. Endpoints des services centraux (les rôles `client_*` en dérivent) -- DNS interne : plancher `/etc/hosts` (`hosts_statiques`) + PowerDNS + `client_unbound` (opt-in). +- DNS interne : plancher `/etc/hosts` (`hosts_statiques`) + PowerDNS + `client_resolveur` (opt-in). - AC/PKI (`client_pki_ca_url` → infra-pki, provisioner). - IdM/LDAP : annuaire résolu par `resoudre_annuaire` (hôte + base DN dérivés du domaine). - Relais courriel (`client_smtp_relais` → edge-mta, MTA Postfix, port 25). @@ -90,7 +90,7 @@ domaines publics, `edge`, autorité DNS, FQDN exposés. | Secrets Vault | **Constante** 🔒 | — (gérés à part, jamais en clair) | | `fuseau_horaire` | Défaut | par hôte (rare) | | `proxmox_clone_noeud` / `stockage` / `pont` | Défaut | par hôte (`serveurs.yml`) | -| DNS internes (plancher `/etc/hosts` + PowerDNS + `client_unbound`) | Défaut | par hôte / groupe | +| DNS internes (plancher `/etc/hosts` + PowerDNS + `client_resolveur`) | Défaut | par hôte / groupe | | Politiques durcissement (SSH, nftables, fail2ban, journald…) | Défaut | par hôte / groupe | | Relais SMTP, TLS internes | Défaut | par hôte / groupe | | `ciuser`, compte `ansible` | Défaut | rarement surchargé | diff --git a/docs/pouvoirs-set-ops.md b/docs/pouvoirs-set-ops.md index 9744593..316392d 100644 --- a/docs/pouvoirs-set-ops.md +++ b/docs/pouvoirs-set-ops.md @@ -94,7 +94,7 @@ prouvés. - **Forge logicielle** : `serveur_forgejo`. - **Observabilité** : `serveur_prometheus`, `serveur_grafana`, `serveur_loki`, `serveur_icinga`, avec les clients `client_metrique`, `client_journal`, `client_supervision`. -- **Intégrations transverses** : `client_pki`, `client_backup`, `client_smtp`, `client_metrique`, `client_journal`, `client_unbound`. +- **Intégrations transverses** : `client_pki`, `client_backup`, `client_smtp`, `client_metrique`, `client_journal`, `client_resolveur`. - **Méta-rôles d'agrégation** : `identity`, `applications`, `database`, `web`, `monitoring`, `backup`, `storage`. diff --git a/docs/registre-flux.md b/docs/registre-flux.md index 4554a89..025f342 100644 --- a/docs/registre-flux.md +++ b/docs/registre-flux.md @@ -12,10 +12,10 @@ | `client_metrique` | ingress | 9100 | tcp | serveur_prometheus | tls | Scrape des métriques par Prometheus (node_exporter en HTTPS). | | `client_pki` | egress | 8443 | tcp | serveur_step_ca | tls-requis | Émission/renouvellement des certificats par ACME et récupération de la racine auprès de l'AC interne. | | `client_smtp` | egress | 25 | tcp | serveur_postfix | starttls | Relais des notifications locales vers le MTA central (Postfix), STARTTLS. | -| `client_unbound` | ingress | 53 | udp | localhost | clair | Résolveur local sur boucle locale (les processus du nœud interrogent 127.0.0.1). | -| `client_unbound` | egress | 53 | tcp | serveur_powerdns | clair | Transfert des requêtes de la zone souveraine vers le DNS autoritatif interne (PowerDNS). | -| `client_unbound` | egress | 53 | udp | externe | clair | Récursion DNS depuis la racine (UDP d'abord), validée par DNSSEC — la confidentialité du transport n'est pas l'enjeu, l'authenticité l'est. | -| `client_unbound` | egress | 53 | tcp | externe | clair | Récursion DNS en TCP : repli obligatoire quand la réponse dépasse la taille UDP (fréquent avec DNSSEC). | +| `client_resolveur` | ingress | 53 | udp | localhost | clair | Résolveur local sur boucle locale (les processus du nœud interrogent 127.0.0.1). | +| `client_resolveur` | egress | 53 | tcp | serveur_powerdns | clair | Transfert des requêtes de la zone souveraine vers le DNS autoritatif interne (PowerDNS). | +| `client_resolveur` | egress | 53 | udp | externe | clair | Récursion DNS depuis la racine (UDP d'abord), validée par DNSSEC — la confidentialité du transport n'est pas l'enjeu, l'authenticité l'est. | +| `client_resolveur` | egress | 53 | tcp | externe | clair | Récursion DNS en TCP : repli obligatoire quand la réponse dépasse la taille UDP (fréquent avec DNSSEC). | | `serveur_backup` | ingress | 22 | tcp | client_backup | ssh | Dépôt restic servi par SSH (utilisateur restreint restic + clé) ; chaque client_backup pousse ses instantanés. | | `serveur_backup` | egress | 5665 | tcp | serveur_icinga | tls-requis | Rapport passif des sauvegardes vers l'API Icinga : le depot est le seul a voir ce qui est reellement arrive. | | `serveur_collabora` | ingress | 9980 | tcp | edge | clair | Éditeur servi au navigateur via l'edge (WebSocket WOPI ; TLS terminé à l'edge). | diff --git a/playbooks/groupes/client_resolveur.yml b/playbooks/groupes/client_resolveur.yml new file mode 100644 index 0000000..e40ad93 --- /dev/null +++ b/playbooks/groupes/client_resolveur.yml @@ -0,0 +1,15 @@ +--- +- name: Appliquer le groupe client_resolveur + hosts: client_resolveur + become: true + gather_facts: true + + pre_tasks: + - name: Vérifier que la cible est Debian + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Debian" + fail_msg: "Ce playbook est prévu pour Debian." + + roles: + - client_resolveur diff --git a/playbooks/groupes/serveur_debian.yml b/playbooks/groupes/serveur_debian.yml index 93a53d1..e9a9ea8 100644 --- a/playbooks/groupes/serveur_debian.yml +++ b/playbooks/groupes/serveur_debian.yml @@ -24,7 +24,7 @@ # effet faute de `resolvconf` (qu'on ne peut pas installer sans résolution — la boucle # se referme). Mesuré le 2026-08-06 sur deux VM neuves. # - # PASSAGE DE RELAIS : `client_unbound` reprend ce fichier plus tard pour le basculer + # PASSAGE DE RELAIS : `client_resolveur` reprend ce fichier plus tard pour le basculer # vers `127.0.0.1`. La garde ci-dessous le respecte — sans elle, chaque déploiement # défairait la bascule, et deux rôles se disputeraient le même fichier sans fin. - name: Lire le résolveur en place @@ -42,7 +42,7 @@ mode: "0644" content: | # Géré par Set-OPS — résolveur d'amorçage (intrant `dns_amorcage`). - # Remplacé par `client_unbound` lorsqu'il bascule vers le résolveur local. + # Remplacé par `client_resolveur` lorsqu'il bascule vers le résolveur local. {% for serveur in dns_amorcage.split(',') if serveur | trim %} nameserver {{ serveur | trim }} {% endfor %} diff --git a/playbooks/groupes/client_unbound.yml b/playbooks/groupes/serveur_resolveur.yml similarity index 50% rename from playbooks/groupes/client_unbound.yml rename to playbooks/groupes/serveur_resolveur.yml index 80e284b..7b0f97b 100644 --- a/playbooks/groupes/client_unbound.yml +++ b/playbooks/groupes/serveur_resolveur.yml @@ -1,10 +1,7 @@ --- -- name: Appliquer le groupe client_unbound - hosts: client_unbound +- name: Appliquer le groupe serveur_resolveur + hosts: serveur_resolveur become: true - # Le verrou dpkg est tenu par les maj automatiques de Debian, par vagues, plusieurs - # minutes apres le premier demarrage. Pose ICI plutot que dans chaque role : toute - # tache apt du play en herite, y compris celles des roles inclus. module_defaults: ansible.builtin.apt: lock_timeout: 300 @@ -19,4 +16,4 @@ fail_msg: "Ce playbook est prévu pour Debian." roles: - - client_unbound + - serveur_resolveur diff --git a/playbooks/site.yml b/playbooks/site.yml index 7e72a06..5aab3d2 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -74,5 +74,5 @@ import_playbook: groupes/client_smtp.yml - name: Couche agents — groupe client_backup import_playbook: groupes/client_backup.yml -- name: Couche agents — groupe client_unbound - import_playbook: groupes/client_unbound.yml +- name: Couche agents — groupe client_resolveur + import_playbook: groupes/client_resolveur.yml diff --git a/roles/client_unbound/README.md b/roles/client_resolveur/README.md similarity index 77% rename from roles/client_unbound/README.md rename to roles/client_resolveur/README.md index 09c0941..d4e854e 100644 --- a/roles/client_unbound/README.md +++ b/roles/client_resolveur/README.md @@ -1,4 +1,4 @@ -# client_unbound +# client_resolveur **Résolveur local** par nœud (Unbound) : cache + récursion, avec une **stub-zone** vers l'autoritatif interne (PowerDNS). Rend la résolution *dynamique* sans casser Internet. @@ -21,8 +21,8 @@ Changer le résolveur d'un nœud à distance, c'est risquer de le rendre muet. L donc **deux** interrupteurs, et vérifie avant d'agir : ```yaml -client_unbound_apply: true -client_unbound_confirm: true +client_resolveur_apply: true +client_resolveur_confirm: true ``` Sans les deux, le rôle installe et démarre Unbound mais **ne touche pas** à `/etc/resolv.conf` @@ -33,18 +33,18 @@ qu'ensuite. Même doctrine que la règle 4 d'`AGENTS.md` : action risquée = con ## Variables | Variable | Défaut | Rôle | | --- | --- | --- | -| `client_unbound_ecoute` | `127.0.0.1` | Écoute (boucle locale) | -| `client_unbound_zone_interne` | `{{ domaine_interne }}` | Zone en stub | -| `client_unbound_dns_autoritatif` | dérivé du groupe `serveur_powerdns` | Cible de la stub-zone | -| `client_unbound_transitaires` | `[]` | Vide = récursion propre (racine + DNSSEC) | -| `client_unbound_apply` / `_confirm` | `false` / `false` | Bascule de `/etc/resolv.conf` | +| `client_resolveur_ecoute` | `127.0.0.1` | Écoute (boucle locale) | +| `client_resolveur_zone_interne` | `{{ domaine_interne }}` | Zone en stub | +| `client_resolveur_dns_autoritatif` | dérivé du groupe `serveur_powerdns` | Cible de la stub-zone | +| `client_resolveur_transitaires` | `[]` | Vide = récursion propre (racine + DNSSEC) | +| `client_resolveur_apply` / `_confirm` | `false` / `false` | Bascule de `/etc/resolv.conf` | ## Flux (`meta/flux.yml`) `53/udp` entrant en **localhost** · `53/tcp` sortant vers `serveur_powerdns`. ## Notes / limites - Transitaires vides = Unbound **récurse lui-même** (racine + DNSSEC). Si la récursion - sortante est bloquée par le réseau, renseigner `client_unbound_transitaires`. + sortante est bloquée par le réseau, renseigner `client_resolveur_transitaires`. - Le rôle exige l'IP de l'autoritatif interne (assertion) : `serveur_powerdns` doit être dans l'inventaire avec un `ansible_host`. - DoT (DNS chiffré) n'est pas encore branché — c'est un des flux restants du zéro-confiance. diff --git a/roles/client_resolveur/defaults/main.yml b/roles/client_resolveur/defaults/main.yml new file mode 100644 index 0000000..bc04aad --- /dev/null +++ b/roles/client_resolveur/defaults/main.yml @@ -0,0 +1,34 @@ +--- +# INTÉGRATION CLIENTE : désigner le résolveur de l'écosystème. +# +# CE RÔLE N'INSTALLE PLUS RIEN (2026-08-24). Il posait un Unbound sur CHAQUE VM — N démons +# identiques, ~21 Mo chacun, pour quelques centaines de requêtes. Le résolveur est +# désormais un service du tenant (`serveur_resolveur`), et ce rôle ne fait plus qu'une +# chose : écrire `/etc/resolv.conf`. +# +# L'ancien nom mentait dès lors qu'il n'installait plus Unbound — d'où `client_resolveur`. + +# DÉRIVÉ de l'inventaire : l'hôte qui porte le résolveur. Aucun résolveur au plan rend une +# valeur vide, et le rôle ne touche à rien (voir `client_resolveur_actif`). +client_resolveur_hote: >- + {{ (groups['serveur_resolveur'] | default([]) | first | default('')) }} +client_resolveur_adresse: >- + {{ hostvars[client_resolveur_hote].ansible_host | default('') + if client_resolveur_hote else '' }} + +# L'INTÉGRATION SUIT L'EXISTENCE DU SERVICE — elle ne se déclare pas. Un écosystème sans +# résolveur garde la résolution d'amorçage posée par cloud-init : c'est un choix valide, +# pas une panne. +client_resolveur_actif: "{{ (groups['serveur_resolveur'] | default([])) | length > 0 }}" + +client_resolveur_zone_interne: "{{ domaine_interne }}" +client_resolveur_resolv_conf: "/etc/resolv.conf" + +# --- BASCULE PROTÉGÉE --------------------------------------------------------- +# +# Réécrire /etc/resolv.conf peut couper toute la flotte si la cible est mauvaise. Le rôle +# exige donc une confirmation ET vérifie que le résolveur répond DÉJÀ — pour l'interne et +# pour l'Internet — avant de basculer quoi que ce soit. +client_resolveur_apply: false +client_resolveur_confirm: false +client_resolveur_attente_reconnexion: 120 diff --git a/roles/client_resolveur/meta/flux.yml b/roles/client_resolveur/meta/flux.yml new file mode 100644 index 0000000..aa40782 --- /dev/null +++ b/roles/client_resolveur/meta/flux.yml @@ -0,0 +1,18 @@ +--- +# Flux réseau du client de résolution. Voir docs/flux-conception.md. +# +# Il n'écoute plus rien : depuis 2026-08-24, le résolveur est un service du tenant et +# non un démon local. L'hôte ne fait qu'interroger. +flux: + - sens: egress + port: 53 + protocole: udp + pair: serveur_resolveur + chiffrement: clair + raison: "Résoudre auprès du résolveur de l'écosystème, et de personne d'autre." + - sens: egress + port: 53 + protocole: tcp + pair: serveur_resolveur + chiffrement: clair + raison: "Réponses longues et bascule TCP, obligatoires en DNS." diff --git a/roles/client_resolveur/meta/integration.yml b/roles/client_resolveur/meta/integration.yml new file mode 100644 index 0000000..7c28a5e --- /dev/null +++ b/roles/client_resolveur/meta/integration.yml @@ -0,0 +1,12 @@ +--- +# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le +# raisonnement, et docs/decisions-architecture.md (D-33). +integration: + universelle: true + raison: >- + Tout hote resout des noms, et doit le faire aupres du resolveur de son ecosysteme + plutot que d'un tiers. Une machine restee sur la resolution d'amorcage envoie + chacune de ses questions dehors, sans que rien ne le signale. + # AUCUNE exemption, pas meme l'hote qui PORTE le resolveur : il se sert lui-meme, et + # c'est le cas le plus simple. L'exempter reviendrait a dire que le resolveur ne se + # fait pas confiance. diff --git a/roles/client_resolveur/tasks/main.yml b/roles/client_resolveur/tasks/main.yml new file mode 100644 index 0000000..bd1a977 --- /dev/null +++ b/roles/client_resolveur/tasks/main.yml @@ -0,0 +1,75 @@ +--- +- name: Exiger un résolveur joignable quand l'intégration est active + ansible.builtin.assert: + that: + - client_resolveur_adresse | length > 0 + fail_msg: >- + client_resolveur est actif mais aucun hôte ne porte `serveur_resolveur`, ou son + adresse est introuvable. Déclarer le service au plan, ou forcer + client_resolveur_actif=false. + when: client_resolveur_actif | bool + +- name: Refuser la bascule du résolveur sans confirmation explicite + ansible.builtin.assert: + that: + - client_resolveur_confirm | bool + fail_msg: "Bascule refusée : définir client_resolveur_confirm=true (avec client_resolveur_apply=true)." + when: client_resolveur_apply | bool + +# ON VÉRIFIE AVANT DE BASCULER, PAS APRÈS. Un resolv.conf qui pointe vers un service muet +# coupe l'hôte de tout — et le diagnostic, lui, exige de résoudre des noms. +- name: Valider que le résolveur répond AVANT de basculer (interne + Internet) + ansible.builtin.command: "dig @{{ client_resolveur_adresse }} {{ item.nom }} {{ item.type }} +short" + register: client_resolveur_validation + changed_when: false + failed_when: client_resolveur_validation.stdout | trim == '' + loop: + - { nom: "{{ client_resolveur_zone_interne }}", type: "SOA" } + - { nom: "deb.debian.org", type: "A" } + loop_control: + label: "{{ item.nom }} {{ item.type }}" + when: + - client_resolveur_actif | bool + - client_resolveur_apply | bool + - client_resolveur_confirm | bool + - not ansible_check_mode + +- name: Désigner le résolveur de l'écosystème + ansible.builtin.copy: + dest: "{{ client_resolveur_resolv_conf }}" + content: | + # Généré par Set-OPS (rôle client_resolveur). Résolveur de l'écosystème. + search {{ client_resolveur_zone_interne }} + nameserver {{ client_resolveur_adresse }} + owner: root + group: root + mode: "0644" + when: + - client_resolveur_actif | bool + - client_resolveur_apply | bool + - client_resolveur_confirm | bool + - not ansible_check_mode + +# RETIRER L'ANCIEN DÉMON LOCAL — APRÈS la bascule, jamais avant. +# +# Sans cette tâche, les N Unbound qu'on voulait supprimer resteraient installés et actifs : +# le rôle aurait changé le `resolv.conf` sans rien économiser, et la raison même du +# changement serait perdue. Une intégration qui ne sait pas se retirer est un piège +# différé. +# +# L'ORDRE COMPTE. Retirer le paquet avant d'avoir écrit le nouveau `resolv.conf` couperait +# l'hôte de toute résolution au milieu du play — et le diagnostic, lui, a besoin de +# résoudre des noms. +# +# L'HÔTE QUI PORTE LE RÉSOLVEUR EST ÉPARGNÉ, évidemment : c'est le même paquet. +- name: Retirer le résolveur local devenu inutile + ansible.builtin.apt: + name: unbound + state: absent + purge: false + when: + - client_resolveur_actif | bool + - client_resolveur_apply | bool + - client_resolveur_confirm | bool + - inventory_hostname not in (groups['serveur_resolveur'] | default([])) + - not ansible_check_mode diff --git a/roles/client_unbound/defaults/main.yml b/roles/client_unbound/defaults/main.yml deleted file mode 100644 index 32b08fe..0000000 --- a/roles/client_unbound/defaults/main.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -# Résolveur local Unbound par nœud : cache + récursion (ou forward), avec une -# stub-zone vers l'autoritatif interne (PowerDNS). Rend la résolution dynamique -# (au lieu du plancher /etc/hosts statique) sans casser Internet. La bascule de -# /etc/resolv.conf est PROTÉGÉE (apply + confirm) — bascule du résolveur validée avant application. -client_unbound_paquets: - - unbound - - bind9-dnsutils # dig, pour valider avant de basculer le resolver -client_unbound_service: "unbound" -client_unbound_ecoute: "127.0.0.1" - -# Zone interne autoritative (PowerDNS) — résolue via une stub-zone. -client_unbound_zone_interne: "{{ domaine_interne }}" -# IP du serveur autoritatif interne. Dérivée du groupe serveur_powerdns. -client_unbound_dns_autoritatif: "{{ hostvars[groups['serveur_powerdns'][0]].ansible_host | default('') if groups.get('serveur_powerdns') else '' }}" - -# Transitaires pour '.' (Internet). Vide => Unbound recurse lui-même (racine + DNSSEC). -# À renseigner si la récursion sortante est bloquée (forward vers l'upstream réseau). -client_unbound_transitaires: [] - -# Bascule de /etc/resolv.conf vers 127.0.0.1 : les DEUX requis (garde-fou anti-coupure). -client_unbound_apply: false -client_unbound_confirm: false -client_unbound_resolv_conf: "/etc/resolv.conf" - -# Delai maximal pour que l'hote reponde apres le redemarrage d'Unbound. Depasser reste -# un ECHEC : on attend une machine qui revient, pas une panne qu'on tait. -client_unbound_attente_reconnexion: 180 diff --git a/roles/client_unbound/handlers/main.yml b/roles/client_unbound/handlers/main.yml deleted file mode 100644 index 12427db..0000000 --- a/roles/client_unbound/handlers/main.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -# `ignore_unreachable` : le redemarrage a deja fait perdre la connexion en cours de -# deploiement — « Connection timed out during banner exchange », le 2026-08-09 sur -# web-dorsal-01. La CAUSE n'est pas etablie : `usedns no` ecarte la resolution inverse -# de sshd, et le journal du deploiement rate a ete ecrase avant d'etre lu. Ce que le -# depot sait deja de ce symptome (Makefile, `_attendre-hote`) : a travers la frontiere, -# le TCP s'etablit par proxy SYN et l'echec se lit ainsi meme quand l'hote n'est -# simplement pas la. -# -# On ne masque donc pas une panne : une perte ICI n'abandonne plus les couches -# suivantes, et `wait_for_connection` exige que l'hote revienne pour de bon. S'il ne -# revient pas, la tache suivante echoue — franchement. -- name: Redémarrer unbound - ansible.builtin.systemd: - name: "{{ client_unbound_service }}" - state: restarted - when: not ansible_check_mode - ignore_unreachable: true diff --git a/roles/client_unbound/meta/flux.yml b/roles/client_unbound/meta/flux.yml deleted file mode 100644 index 3983d0c..0000000 --- a/roles/client_unbound/meta/flux.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -# Flux réseau du résolveur local Unbound (opt-in). Voir docs/flux-conception.md. -flux: - - sens: ingress - port: 53 - protocole: udp - pair: localhost - chiffrement: clair - raison: "Résolveur local sur boucle locale (les processus du nœud interrogent 127.0.0.1)." - - sens: egress - port: 53 - protocole: tcp - pair: serveur_powerdns - chiffrement: clair - raison: "Transfert des requêtes de la zone souveraine vers le DNS autoritatif interne (PowerDNS)." - - # Récursion sortante : `client_unbound_transitaires` est vide par défaut, donc Unbound - # interroge lui-même la racine puis les autoritatifs — c'est le choix souverain, sans - # dépendre du résolveur d'un fournisseur. Renseigner des transitaires rendrait ces deux - # flux inutiles, mais les laisser déclarés ne coûte rien et évite une panne muette. - - sens: egress - port: 53 - protocole: udp - pair: externe - chiffrement: clair - raison: "Récursion DNS depuis la racine (UDP d'abord), validée par DNSSEC — la confidentialité du transport n'est pas l'enjeu, l'authenticité l'est." - - sens: egress - port: 53 - protocole: tcp - pair: externe - chiffrement: clair - raison: "Récursion DNS en TCP : repli obligatoire quand la réponse dépasse la taille UDP (fréquent avec DNSSEC)." diff --git a/roles/client_unbound/meta/integration.yml b/roles/client_unbound/meta/integration.yml deleted file mode 100644 index a051bde..0000000 --- a/roles/client_unbound/meta/integration.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -# Politique d'integration. Declaree ICI, une fois, plutot que recopiee sur chaque -# serveur du plan. Voir docs/integrations-vm.md et docs/decisions-architecture.md (D-33). -integration: - universelle: true - raison: >- - Tout hote doit resoudre des noms — ne serait-ce que pour `apt`. Le DNS - autoritatif du tenant (PowerDNS) ne repond QUE pour la zone souveraine et - refuse le reste : sans resolveur recursif, une machine ne peut meme pas - s'installer. `client_unbound` delegue la zone interne a l'autoritatif et - recurse depuis la racine, sans dependre du resolveur d'un fournisseur. - # Le serveur autoritatif est exempte : PowerDNS occupe deja le port 53 sur cet - # hote. Y ajouter Unbound produirait un conflit de liaison, pas un resolveur. - # Il resout par le resolveur d'amorcage (`proxmox_clone_dns`), qui lui suffit : - # ses seuls besoins externes sont apt et NTP. - sauf_role: serveur_powerdns diff --git a/roles/client_unbound/tasks/main.yml b/roles/client_unbound/tasks/main.yml deleted file mode 100644 index d847b1a..0000000 --- a/roles/client_unbound/tasks/main.yml +++ /dev/null @@ -1,83 +0,0 @@ ---- -- name: Exiger l'IP de l'autoritatif interne (PowerDNS) - ansible.builtin.assert: - that: - - client_unbound_dns_autoritatif | length > 0 - fail_msg: "client_unbound_dns_autoritatif requis (aucun hôte serveur_powerdns actif ?)." - -- name: Installer Unbound - ansible.builtin.apt: - name: "{{ client_unbound_paquets }}" - state: present - update_cache: true - cache_valid_time: 3600 - -- name: Déployer la configuration Unbound (stub-zone interne + récursion) - ansible.builtin.template: - src: setops.conf.j2 - dest: /etc/unbound/unbound.conf.d/setops.conf - owner: root - group: root - mode: "0644" - notify: Redémarrer unbound - -- name: Valider la configuration Unbound - ansible.builtin.command: unbound-checkconf - changed_when: false - when: not ansible_check_mode - -- name: Activer et démarrer Unbound - ansible.builtin.systemd: - name: "{{ client_unbound_service }}" - enabled: true - state: started - when: not ansible_check_mode - -- name: Appliquer les redémarrages Unbound avant toute validation/bascule - ansible.builtin.meta: flush_handlers - -# Attendre une CONDITION — que l'hote reponde — et non une duree. Si la connexion a -# saute pendant le redemarrage, on la retablit ici ; sinon la tache passe aussitot. -- name: Exiger que l hote reponde avant de poursuivre - ansible.builtin.wait_for_connection: - timeout: "{{ client_unbound_attente_reconnexion }}" - sleep: 5 - when: not ansible_check_mode - -# --- Bascule du resolver : PROTÉGÉE + validée (ne casse jamais un nœud) --- -- name: Refuser la bascule du resolver sans confirmation explicite - ansible.builtin.assert: - that: - - client_unbound_confirm | bool - fail_msg: "Bascule refusée : définir client_unbound_confirm=true (avec client_unbound_apply=true)." - when: client_unbound_apply | bool - -- name: Valider qu'Unbound résout AVANT de basculer (interne + Internet) - ansible.builtin.command: "dig @127.0.0.1 {{ item.nom }} {{ item.type }} +short" - register: client_unbound_validation - changed_when: false - failed_when: client_unbound_validation.stdout | trim == '' - loop: - - { nom: "{{ client_unbound_zone_interne }}", type: "SOA" } - - { nom: "deb.debian.org", type: "A" } - loop_control: - label: "{{ item.nom }} {{ item.type }}" - when: - - client_unbound_apply | bool - - client_unbound_confirm | bool - - not ansible_check_mode - -- name: Basculer /etc/resolv.conf vers Unbound local (127.0.0.1) - ansible.builtin.copy: - dest: "{{ client_unbound_resolv_conf }}" - content: | - # Généré par Set-OPS (rôle client_unbound). Résolveur local Unbound. - search {{ client_unbound_zone_interne }} - nameserver 127.0.0.1 - owner: root - group: root - mode: "0644" - when: - - client_unbound_apply | bool - - client_unbound_confirm | bool - - not ansible_check_mode diff --git a/roles/client_unbound/templates/setops.conf.j2 b/roles/client_unbound/templates/setops.conf.j2 deleted file mode 100644 index eb4eeca..0000000 --- a/roles/client_unbound/templates/setops.conf.j2 +++ /dev/null @@ -1,25 +0,0 @@ -# Géré par Set-OPS (rôle client_unbound). Ne pas éditer à la main. -# Résolveur local : cache + récursion (ou forward), stub-zone vers l'autoritatif interne. -server: - interface: {{ client_unbound_ecoute }} - access-control: 127.0.0.0/8 allow - hide-identity: yes - hide-version: yes - prefetch: yes - do-ip6: no - # La zone interne n'est pas signée DNSSEC (PowerDNS autoritatif, dnssec off). - domain-insecure: "{{ client_unbound_zone_interne }}" - -# Zone interne : déléguée à l'autoritatif (PowerDNS). -stub-zone: - name: "{{ client_unbound_zone_interne }}" - stub-addr: {{ client_unbound_dns_autoritatif }} -{% if client_unbound_transitaires | length > 0 %} - -# Internet : forward vers l'upstream réseau (au lieu de la récursion directe). -forward-zone: - name: "." -{% for t in client_unbound_transitaires %} - forward-addr: {{ t }} -{% endfor %} -{% endif %} diff --git a/roles/hosts_statiques/README.md b/roles/hosts_statiques/README.md index 1f69dcd..2047ce6 100644 --- a/roles/hosts_statiques/README.md +++ b/roles/hosts_statiques/README.md @@ -18,7 +18,7 @@ C'est ce qui rend l'**ordre de reconstruction** possible : appliqué dans la cou déploiement d'une instance neuve. Les trois couches de résolution : ce plancher → PowerDNS autoritatif → Unbound local -(`client_unbound`, opt-in). Cf. `docs/dns-interne.md`. +(`client_resolveur`, opt-in). Cf. `docs/dns-interne.md`. ## Variables | Variable | Défaut | Rôle | diff --git a/roles/serveur_powerdns/defaults/main.yml b/roles/serveur_powerdns/defaults/main.yml index aeba88d..4aa8589 100644 --- a/roles/serveur_powerdns/defaults/main.yml +++ b/roles/serveur_powerdns/defaults/main.yml @@ -19,10 +19,20 @@ serveur_powerdns_soa_expire: 1209600 serveur_powerdns_soa_minimum: 3600 # L'adresse du tenant SEULEMENT, pas `0.0.0.0` : lier toutes les interfaces occupe aussi # `127.0.0.1:53`, ou un resolveur recursif local voudrait s'installer. Ce n'est pas un -# probleme theorique — c'est la raison pour laquelle `client_unbound` exempte cet hote. +# probleme theorique — c'est la raison pour laquelle `client_resolveur` exempte cet hote. # PowerDNS reste autoritatif pur : il repond pour la zone souveraine et REFUSE le reste. -serveur_powerdns_listen_addresses: - - "{{ ansible_host | default('0.0.0.0') }}" +# LE RESOLVEUR DU TENANT PARTAGE CET HOTE (2026-08-24). Quand c'est le cas, l'autoritatif +# lui laisse le port 53 de l'adresse LAN et se replie sur la loopback, sur un autre port : +# le resolveur devient la SEULE porte, et personne n'interroge l'autoritatif directement. +# +# Derive de l'inventaire, pas declare a la main : l'exploitant ne doit pas avoir a se +# souvenir de deplacer un port parce qu'il a ajoute un role. +serveur_powerdns_colocalise_resolveur: >- + {{ inventory_hostname in (groups['serveur_resolveur'] | default([])) }} +serveur_powerdns_listen_port: "{{ 5300 if (serveur_powerdns_colocalise_resolveur | bool) else 53 }}" +serveur_powerdns_listen_addresses: >- + {{ ['127.0.0.1'] if (serveur_powerdns_colocalise_resolveur | bool) + else [ansible_host | default('0.0.0.0')] }} serveur_powerdns_allow_axfr_ips: [] # Enregistrements additionnels geres explicitement. diff --git a/roles/serveur_powerdns/meta/flux.yml b/roles/serveur_powerdns/meta/flux.yml index 6872fce..4860739 100644 --- a/roles/serveur_powerdns/meta/flux.yml +++ b/roles/serveur_powerdns/meta/flux.yml @@ -1,21 +1,31 @@ --- # Flux réseau du DNS autoritatif interne (PowerDNS). Voir docs/flux-conception.md. +# LE PORT DE L'AUTORITATIF EST DÉRIVÉ DEPUIS LE 2026-08-24, et c'est pour ça qu'il est +# déclaré `derive` plutôt que `53` : +# +# seul sur son hôte -> 53 sur l'adresse du tenant +# partageant l'hôte avec le résolveur -> 5300 sur la loopback, derrière lui +# +# Le résolveur devient alors la seule porte, et personne n'interroge l'autoritatif +# directement. `verifier_ports` traite un port non numérique comme « pas une écoute fixe » +# — ce qui est exactement le cas : les deux services lient le port 53 mais sur des +# ADRESSES différentes, ce qu'une comparaison de ports seuls ne peut pas distinguer. flux: - sens: ingress - port: 53 + port: derive protocole: udp pair: flotte chiffrement: clair - raison: "Résolution DNS interne (zone souveraine). DoT/DoH = feuille de route (chiffrement DNS)." + raison: "Zone souveraine. 53 seul sur son hôte, 5300 sur la loopback derrière le résolveur." - sens: ingress - port: 53 + port: derive protocole: tcp pair: flotte chiffrement: clair - raison: "Résolution DNS interne en TCP (réponses volumineuses, AXFR restreint par allow_axfr_ips)." + raison: "Idem en TCP (réponses volumineuses, AXFR restreint par allow_axfr_ips)." # L'autoritatif ne RÉCURSE pour personne — mais il doit résoudre POUR LUI-MÊME : `apt`, - # NTP, les certificats. Il est le seul hôte exempté de `client_unbound` (PowerDNS occupe + # NTP, les certificats. Il est le seul hôte exempté de `client_resolveur` (PowerDNS occupe # déjà son port 53), donc rien d'autre ne lui ouvre ce flux. Sans cette déclaration, la # frontière jette ses requêtes et la machine ne peut même pas s'installer — mesuré le # 2026-08-06, sur la VM qui devait justement servir de DNS au tenant. diff --git a/roles/serveur_powerdns/templates/setops-bind.conf.j2 b/roles/serveur_powerdns/templates/setops-bind.conf.j2 index a295cba..35d8485 100644 --- a/roles/serveur_powerdns/templates/setops-bind.conf.j2 +++ b/roles/serveur_powerdns/templates/setops-bind.conf.j2 @@ -2,6 +2,9 @@ Ne PAS le redeclarer ici : sinon PowerDNS refuse « multiple backends 'bind' ». #} bind-config={{ serveur_powerdns_bind_config }} local-address={{ serveur_powerdns_listen_addresses | join(',') }} +# Le port suit l'adresse : 53 quand l'autoritatif est seul sur son hote, 5300 quand le +# resolveur du tenant partage la machine et prend le port 53 de l'adresse LAN. +local-port={{ serveur_powerdns_listen_port }} {% if serveur_powerdns_allow_axfr_ips | length > 0 %} allow-axfr-ips={{ serveur_powerdns_allow_axfr_ips | join(',') }} disable-axfr=no diff --git a/roles/serveur_resolveur/README.md b/roles/serveur_resolveur/README.md new file mode 100644 index 0000000..a269401 --- /dev/null +++ b/roles/serveur_resolveur/README.md @@ -0,0 +1,66 @@ +# serveur_resolveur + +**Le résolveur de l'écosystème** : un seul Unbound pour tout le tenant, récursif depuis +les serveurs racine, qui délègue la zone souveraine à PowerDNS. + +## Pourquoi un seul, et pourquoi ici + +Avant le 2026-08-24, `client_unbound` posait un Unbound sur **chaque VM**. Ça marchait et +c'était parfaitement étanche — mais c'était N démons identiques pour un service unique. + +Mesure prise avant de décider : **~21 Mo de RSS par VM**, et **28 à 189 requêtes** servies +depuis le démarrage. Le gain de cache mutualisé est donc négligeable ; ce qu'on récupère, +c'est un démon au lieu de quatorze, et **un endroit à regarder** au lieu de quatorze. + +## Pourquoi pas sur la frontière + +Un résolveur partagé par tout le SITE — l'OPNsense — aurait été le geste le plus économe. +Il devrait alors connaître la zone interne de **chaque tenant** : sans vues par réseau +soigneusement réglées, le tenant A résoudrait les noms du tenant B. C'est l'étanchéité +qu'on protège partout ailleurs. + +Et un tenant dont le résolveur vit chez l'hébergeur **ne peut plus s'émanciper avec** +(voir `docs/filiation-emancipation.md`). + +La récursion est générique ; **la zone interne ne l'est pas**. C'est ce qui fait du +résolveur un service de tenant, et non de fabric. + +## La cohabitation avec l'autoritatif + +Les deux vivent sur `infra-dns-01` et voudraient le port 53. Le partage est **dérivé, pas +déclaré** — l'exploitant n'a pas à se souvenir de déplacer un port parce qu'il a ajouté un +rôle : + +``` +resolveur (Unbound) 10.29.19.11:53 + 127.0.0.1:53 la seule porte +autoritatif (PowerDNS) 127.0.0.1:5300 derrière lui +``` + +Personne n'interroge l'autoritatif directement. Le résolveur le prend en `stub-zone`. + +## Aucun transitaire + +`forward-addr` reste vide : Unbound récurse depuis la racine. Poser un transitaire +reviendrait à confier chaque question de l'écosystème à un tiers — ce que la bascule du +2026-08-23 avait justement supprimé. + +## Ce qu'il vérifie avant de se déclarer bon + +Il interroge sa propre adresse pour la **zone interne** *et* pour un nom de l'**Internet**. +Un résolveur « active » qui ne répond pas est le pire des cas : toute la flotte pointe vers +lui, et découvre la panne en essayant de résoudre — c'est-à-dire au moment où le +diagnostic lui-même devient impossible. + +## Variables principales + +| Variable | Défaut | Rôle | +|---|---|---| +| `serveur_resolveur_ecoutes` | loopback + IP du tenant | où il répond | +| `serveur_resolveur_reseaux_autorises` | le supernet du tenant | qui a le droit de demander | +| `serveur_resolveur_autoritatif` | `127.0.0.1:5300` | la zone souveraine | +| `serveur_resolveur_transitaires` | `[]` | vide = récursion depuis la racine | + +## Ce que ce rôle ne fait pas + +Il ne bascule aucun `/etc/resolv.conf` — c'est le travail de `client_resolveur`, qui +valide que ce service répond **avant** de désigner qui que ce soit. diff --git a/roles/serveur_resolveur/defaults/main.yml b/roles/serveur_resolveur/defaults/main.yml new file mode 100644 index 0000000..6181e07 --- /dev/null +++ b/roles/serveur_resolveur/defaults/main.yml @@ -0,0 +1,44 @@ +--- +# LE RÉSOLVEUR DE L'ÉCOSYSTÈME — un seul, pour tout le tenant. +# +# Avant : un Unbound sur CHAQUE VM, écoutant sur 127.0.0.1. Ça marchait, et c'était +# parfaitement étanche — mais c'était N démons identiques pour un service unique. +# +# MESURE DU 2026-08-24, avant de décider : ~21 Mo de RSS par VM, et entre 28 et 189 +# requêtes servies depuis le démarrage. Le gain de cache mutualisé est donc négligeable ; +# ce qu'on récupère, c'est un démon au lieu de quatorze, et un endroit à regarder au lieu +# de quatorze. +# +# POURQUOI PAS SUR LA FRONTIÈRE (OPNsense), qui aurait été le geste le plus économe : un +# résolveur partagé par tout le SITE devrait connaître la zone interne de CHAQUE tenant. +# Sans vues par réseau soigneusement réglées, le tenant A résoudrait les noms du tenant B +# — l'étanchéité qu'on protège partout ailleurs. Et un tenant dont le résolveur vit chez +# l'hébergeur ne peut plus s'émanciper avec (docs/filiation-emancipation.md). +# +# La récursion est générique ; la ZONE INTERNE ne l'est pas. C'est ce qui fait du +# résolveur un service de TENANT, et non de fabric. + +serveur_resolveur_paquets: + - unbound +serveur_resolveur_service: "unbound" + +# Il écoute pour toute la flotte du tenant — plus seulement pour lui-même. +serveur_resolveur_ecoutes: + - "127.0.0.1" + - "{{ ansible_host }}" + +# QUI A LE DROIT DE L'INTERROGER : le supernet du tenant, dérivé du seed comme tout le +# reste. Un résolveur ouvert au monde est un relais d'amplification. +serveur_resolveur_reseaux_autorises: + - "{{ setops_supernet | default('127.0.0.0/8') }}" + +# La zone souveraine et son autoritatif. PowerDNS se replie sur la loopback quand il +# partage cet hôte (voir serveur_powerdns/defaults), pour lui laisser le port 53. +serveur_resolveur_zone_interne: "{{ domaine_interne }}" +serveur_resolveur_autoritatif: "127.0.0.1" +serveur_resolveur_autoritatif_port: 5300 + +# AUCUN TRANSITAIRE : Unbound récurse depuis les serveurs racine. Poser un transitaire ici +# reviendrait à confier chaque question de l'écosystème à un tiers — ce que la bascule du +# 2026-08-23 avait justement supprimé. +serveur_resolveur_transitaires: [] diff --git a/roles/serveur_resolveur/handlers/main.yml b/roles/serveur_resolveur/handlers/main.yml new file mode 100644 index 0000000..fd768a4 --- /dev/null +++ b/roles/serveur_resolveur/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Redémarrer le résolveur + when: not ansible_check_mode + ansible.builtin.systemd: + name: "{{ serveur_resolveur_service }}" + state: restarted diff --git a/roles/serveur_resolveur/meta/authentification.yml b/roles/serveur_resolveur/meta/authentification.yml new file mode 100644 index 0000000..8b9bd1c --- /dev/null +++ b/roles/serveur_resolveur/meta/authentification.yml @@ -0,0 +1,13 @@ +--- +# Position de ce role dans la directive d'authentification (D-38..D-41). +# Voir docs/authentification.md. Gardee par la preuve P29. +authentification: + portee: sans-auth-humaine + mecanisme: aucun + formulaire_local: sans-objet + secours: "Acces SSH a l'hote" + raison: >- + Un resolveur DNS ne sert que des machines et n'expose aucune interface humaine. + Ce qui le protege n'est pas une authentification mais sa LISTE D'ACCES : il ne + repond qu'au supernet du tenant. Un resolveur ouvert au monde serait un relais + d'amplification, pas un service mal authentifie. diff --git a/roles/serveur_resolveur/meta/empreinte.yml b/roles/serveur_resolveur/meta/empreinte.yml new file mode 100644 index 0000000..c9594d0 --- /dev/null +++ b/roles/serveur_resolveur/meta/empreinte.yml @@ -0,0 +1,8 @@ +--- +# Empreinte ressources — mesure du 2026-08-24 : ~21 Mo de RSS pour un Unbound servant +# une VM. Servir la flotte entiere n'y change presque rien : le cout est le cache, et le +# trafic mesure etait de quelques centaines de requetes. +setops_empreinte: + coeurs: 0 + memoire_mo: 64 + disque_go: 0 diff --git a/roles/serveur_resolveur/meta/flux.yml b/roles/serveur_resolveur/meta/flux.yml new file mode 100644 index 0000000..994234f --- /dev/null +++ b/roles/serveur_resolveur/meta/flux.yml @@ -0,0 +1,23 @@ +--- +# Flux réseau du résolveur du tenant. Voir docs/flux-conception.md. +flux: + - sens: ingress + port: 53 + protocole: udp + pair: flotte + chiffrement: clair + raison: "Toute la flotte du tenant résout ici — et nulle part ailleurs." + - sens: ingress + port: 53 + protocole: tcp + pair: flotte + chiffrement: clair + raison: "Réponses longues et bascule TCP, obligatoires en DNS." + - sens: egress + port: 53 + protocole: udp + pair: externe + chiffrement: clair + raison: >- + Récursion depuis les serveurs racine. Aucun transitaire : l'écosystème ne confie + ses questions à personne. diff --git a/roles/serveur_resolveur/tasks/main.yml b/roles/serveur_resolveur/tasks/main.yml new file mode 100644 index 0000000..0c55191 --- /dev/null +++ b/roles/serveur_resolveur/tasks/main.yml @@ -0,0 +1,55 @@ +--- +- name: Exiger la zone souveraine et un réseau autorisé + ansible.builtin.assert: + that: + - serveur_resolveur_zone_interne | length > 0 + - serveur_resolveur_reseaux_autorises | length > 0 + fail_msg: >- + serveur_resolveur exige `domaine_interne` et au moins un réseau dans + `serveur_resolveur_reseaux_autorises`. Un résolveur sans liste d'accès serait + soit muet, soit un relais ouvert. + +- name: Installer le résolveur + ansible.builtin.apt: + name: "{{ serveur_resolveur_paquets }}" + state: present + update_cache: true + when: not ansible_check_mode + +- name: Déployer la configuration Set-OPS + ansible.builtin.template: + src: setops.conf.j2 + dest: /etc/unbound/unbound.conf.d/setops.conf + owner: root + group: root + mode: "0644" + notify: Redémarrer le résolveur + +- name: Valider la configuration avant de la servir + ansible.builtin.command: unbound-checkconf + changed_when: false + when: not ansible_check_mode + +- name: Activer et démarrer le résolveur + ansible.builtin.systemd: + name: "{{ serveur_resolveur_service }}" + enabled: true + state: started + when: not ansible_check_mode + +- name: Appliquer les redémarrages avant de vérifier + ansible.builtin.meta: flush_handlers + +# ÉCRIRE, PUIS RELIRE (D-68). Un résolveur « active » qui ne répond pas est le pire des +# cas : toute la flotte pointe vers lui, et découvre la panne en essayant de résoudre. +- name: Répond-il vraiment, pour l'interne ET pour l'Internet ? + ansible.builtin.command: "dig @{{ ansible_host }} {{ item.nom }} {{ item.type }} +short" + register: serveur_resolveur_controle + changed_when: false + failed_when: serveur_resolveur_controle.stdout | trim == '' + loop: + - { nom: "{{ serveur_resolveur_zone_interne }}", type: "SOA" } + - { nom: "deb.debian.org", type: "A" } + loop_control: + label: "{{ item.nom }} {{ item.type }}" + when: not ansible_check_mode diff --git a/roles/serveur_resolveur/templates/setops.conf.j2 b/roles/serveur_resolveur/templates/setops.conf.j2 new file mode 100644 index 0000000..22b110a --- /dev/null +++ b/roles/serveur_resolveur/templates/setops.conf.j2 @@ -0,0 +1,37 @@ +# GÉNÉRÉ par Set-OPS (rôle serveur_resolveur). NE PAS éditer à la main. +server: +{% for a in serveur_resolveur_ecoutes %} + interface: {{ a }} +{% endfor %} +{% for r in serveur_resolveur_reseaux_autorises %} + access-control: {{ r }} allow +{% endfor %} + access-control: 127.0.0.0/8 allow + hide-identity: yes + hide-version: yes + prefetch: yes + do-ip6: no + # La zone souveraine n'est pas signée : on ne la soumet pas à la validation DNSSEC. + domain-insecure: "{{ serveur_resolveur_zone_interne }}" + # UNBOUND REFUSE D'INTERROGER UNE LOOPBACK PAR DÉFAUT (2026-08-24). + # + # `do-not-query-localhost` vaut `yes` d'origine — une protection contre les boucles. + # Or l'autoritatif de l'écosystème vit désormais SUR 127.0.0.1:5300, derrière ce + # résolveur. Sans cette ligne, toute la zone souveraine rend SERVFAIL. + # + # Et la panne était MASQUÉE : le plancher /etc/hosts répondait pour les noms de la + # flotte, si bien qu'un `getent hosts forge.genese.internal` semblait prouver que la + # résolution marchait. Seul un `dig` explicite l'a montrée. + do-not-query-localhost: no + +stub-zone: + name: "{{ serveur_resolveur_zone_interne }}" + stub-addr: {{ serveur_resolveur_autoritatif }}@{{ serveur_resolveur_autoritatif_port }} +{% if serveur_resolveur_transitaires %} + +forward-zone: + name: "." +{% for f in serveur_resolveur_transitaires %} + forward-addr: {{ f }} +{% endfor %} +{% endif %} diff --git a/scripts/inventory_host.py b/scripts/inventory_host.py index a7c1f8b..95abd6f 100644 --- a/scripts/inventory_host.py +++ b/scripts/inventory_host.py @@ -166,7 +166,7 @@ CHAMPS_PROXMOX: tuple[tuple, ...] = ( ("SETOPS_NOEUD", "proxmox_noeud", False), ("SETOPS_COEURS", "proxmox_coeurs", False), ("SETOPS_MEMOIRE", "proxmox_memoire", False), - # Resolveur d'AMORCAGE, pose par cloud-init. Il ne sert qu'une fois : `client_unbound` + # Resolveur d'AMORCAGE, pose par cloud-init. Il ne sert qu'une fois : `client_resolveur` # bascule ensuite `/etc/resolv.conf` vers 127.0.0.1. Mais sans lui, la VM nait sans # resolution de noms et `apt` ne peut meme pas installer Unbound — l'amorcage # n'aboutit jamais. Le DNS autoritatif du tenant ne convient PAS : il refuse tout ce diff --git a/scripts/inventory_rules.py b/scripts/inventory_rules.py index ea01b58..d144a25 100644 --- a/scripts/inventory_rules.py +++ b/scripts/inventory_rules.py @@ -181,7 +181,7 @@ def integrations_universelles(racine: Path | None = None) -> dict[str, dict]: Une integration universelle n'est PAS recopiee sur chaque serveur du plan : le role la declare une fois dans roles//meta/integration.yml, et tout hote la recoit. Le plan ne porte plus que les integrations reellement facultatives - (client_backup, client_smtp, client_unbound) et les exemptions. + (client_backup, client_smtp, client_resolveur) et les exemptions. Pourquoi : recopiee 14 fois, la meme ligne finit par manquer une quinzieme — et un hote non supervise, non journalise ou sans certificat ne proteste pas. diff --git a/scripts/tests/test_gui_intrants.py b/scripts/tests/test_gui_intrants.py index 057457c..ee85306 100644 --- a/scripts/tests/test_gui_intrants.py +++ b/scripts/tests/test_gui_intrants.py @@ -37,7 +37,7 @@ SOURCE = """# Intrants d'IDENTITE de l'instance — SOURCE UNIQUE. --- domaine_interne: chezlepro.internal -# Resolveur d'AMORCAGE, pose par cloud-init. Il ne sert qu'une fois : `client_unbound` +# Resolveur d'AMORCAGE, pose par cloud-init. Il ne sert qu'une fois : `client_resolveur` # bascule ensuite /etc/resolv.conf vers 127.0.0.1. Sans lui, la VM nait sans resolution. dns_amorcage: 9.9.9.9,149.112.112.112 fuseau_horaire: America/Toronto diff --git a/wiki/DNS-et-résolution.md b/wiki/DNS-et-résolution.md index 33f6577..c95770e 100644 --- a/wiki/DNS-et-résolution.md +++ b/wiki/DNS-et-résolution.md @@ -28,7 +28,7 @@ chercher* la réponse pour toi »). |---|---|---| | **1. Le plancher** | `hosts_statiques` (socle) → `/etc/hosts` sur **chaque** nœud | résout **même DNS éteint**, dès le bootstrap. Le filet en dessous de tout. | | **2. Autoritatif** | `serveur_powerdns` (PowerDNS) | la zone interne, les enregistrements **A**. | -| **3. Récursif local** | `client_unbound` (**opt-in**) | résolveur local : stub-zone → PowerDNS pour l'interne, récursion pour le reste. | +| **3. Récursif local** | `client_resolveur` (**opt-in**) | résolveur local : stub-zone → PowerDNS pour l'interne, récursion pour le reste. | Le **plancher** est le cœur pédagogique : parce que chaque nœud connaît *tout l'écosystème* par `/etc/hosts`, **rien ne dépend du DNS pour démarrer** — PowerDNS devient une *commodité*, pas un @@ -65,7 +65,7 @@ Tu as appris **la hiérarchie de résolution** (hosts → récursif → autorita dig @infra-dns-01.lab.chezlepro.internal lab.chezlepro.internal SOA +short ``` 3. **Vois les couches.** Compare `getent hosts` (plancher) et `dig` (DNS) : **deux chemins**, même IP. -4. **Casse & répare.** Sur un nœud **sans** `client_unbound`, vide `/etc/hosts` de ses entrées +4. **Casse & répare.** Sur un nœud **sans** `client_resolveur`, vide `/etc/hosts` de ses entrées `chezlepro` (garde une sauvegarde !) et coupe l'accès au DNS : la résolution interne **échoue**. Restaure `/etc/hosts` : ça remarche **sans DNS**. Tu viens de *sentir* pourquoi le plancher est le filet de sécurité. @@ -73,6 +73,6 @@ Tu as appris **la hiérarchie de résolution** (hosts → récursif → autorita --- ## Pour aller plus loin *(dépôt)* -- Rôles : `roles/hosts_statiques` (le plancher), `roles/serveur_powerdns`, `roles/client_unbound`. +- Rôles : `roles/hosts_statiques` (le plancher), `roles/serveur_powerdns`, `roles/client_resolveur`. - Conception des 3 couches + frontière publique : `docs/dns-interne.md`. -- Note : `client_unbound` est une **liaison optionnelle** (opt-in) — voir l'unité *Liaisons*. +- Note : `client_resolveur` est une **liaison optionnelle** (opt-in) — voir l'unité *Liaisons*.