From db2d6f6bf0372019d32483630d5f7bff265462c1 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 24 Aug 2026 16:59:34 -0400 Subject: [PATCH] resolveur : un par tenant, et non plus un par machine Mesure avant de decider : ~21 Mo de RSS par VM pour 28 a 189 requetes servies. Le gain de cache est negligeable ; ce qu'on recupere, c'est un demon au lieu de cinq et un endroit a regarder au lieu de cinq. Pas sur la frontiere : un resolveur de SITE devrait connaitre la zone interne de chaque tenant, et un tenant dont le resolveur vit chez l'hebergeur ne peut plus s'emanciper avec. La recursion est generique, la zone interne ne l'est pas. L'autoritatif se replie sur 127.0.0.1:5300 -- derive de la colocalisation, pas declare a la main. Son port devient `derive` dans meta/flux.yml : les deux lient 53 mais sur des adresses differentes. Deux pieges. Unbound refuse d'interroger une loopback par defaut : sans lever do-not-query-localhost, toute la zone rendait SERVFAIL. Et le plancher /etc/hosts MASQUAIT la panne -- getent repondait, dig disait SERVFAIL. La tache de validation du role avait raison contre moi. client_unbound n'installant plus Unbound, son nom mentait : client_resolveur. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 65 +++++++++++++++ docs/audit/plan-de-recette.md | 2 +- docs/audit/preuve-2026-08-24.md | 22 ++--- docs/carte-set-ops.md | 2 +- docs/catalogue-services.md | 9 +- docs/couches-deploiement.yml | 5 +- docs/dependances-groupes.yml | 8 ++ docs/dns-interne.md | 12 +-- docs/frontiere-opnsense.md | 2 +- docs/integrations-vm.md | 2 +- docs/intrants-communs.md | 4 +- docs/pouvoirs-set-ops.md | 2 +- docs/registre-flux.md | 8 +- playbooks/groupes/client_resolveur.yml | 15 ++++ playbooks/groupes/serveur_debian.yml | 4 +- ...ient_unbound.yml => serveur_resolveur.yml} | 9 +- playbooks/site.yml | 4 +- .../README.md | 18 ++-- roles/client_resolveur/defaults/main.yml | 34 ++++++++ roles/client_resolveur/meta/flux.yml | 18 ++++ roles/client_resolveur/meta/integration.yml | 12 +++ roles/client_resolveur/tasks/main.yml | 75 +++++++++++++++++ roles/client_unbound/defaults/main.yml | 28 ------- roles/client_unbound/handlers/main.yml | 18 ---- roles/client_unbound/meta/flux.yml | 32 ------- roles/client_unbound/meta/integration.yml | 16 ---- roles/client_unbound/tasks/main.yml | 83 ------------------- roles/client_unbound/templates/setops.conf.j2 | 25 ------ roles/hosts_statiques/README.md | 2 +- roles/serveur_powerdns/defaults/main.yml | 16 +++- roles/serveur_powerdns/meta/flux.yml | 20 +++-- .../templates/setops-bind.conf.j2 | 3 + roles/serveur_resolveur/README.md | 66 +++++++++++++++ roles/serveur_resolveur/defaults/main.yml | 44 ++++++++++ roles/serveur_resolveur/handlers/main.yml | 6 ++ .../meta/authentification.yml | 13 +++ roles/serveur_resolveur/meta/empreinte.yml | 8 ++ roles/serveur_resolveur/meta/flux.yml | 23 +++++ roles/serveur_resolveur/tasks/main.yml | 55 ++++++++++++ .../templates/setops.conf.j2 | 37 +++++++++ scripts/inventory_host.py | 2 +- scripts/inventory_rules.py | 2 +- scripts/tests/test_gui_intrants.py | 2 +- wiki/DNS-et-résolution.md | 8 +- 44 files changed, 571 insertions(+), 270 deletions(-) create mode 100644 playbooks/groupes/client_resolveur.yml rename playbooks/groupes/{client_unbound.yml => serveur_resolveur.yml} (50%) rename roles/{client_unbound => client_resolveur}/README.md (77%) create mode 100644 roles/client_resolveur/defaults/main.yml create mode 100644 roles/client_resolveur/meta/flux.yml create mode 100644 roles/client_resolveur/meta/integration.yml create mode 100644 roles/client_resolveur/tasks/main.yml delete mode 100644 roles/client_unbound/defaults/main.yml delete mode 100644 roles/client_unbound/handlers/main.yml delete mode 100644 roles/client_unbound/meta/flux.yml delete mode 100644 roles/client_unbound/meta/integration.yml delete mode 100644 roles/client_unbound/tasks/main.yml delete mode 100644 roles/client_unbound/templates/setops.conf.j2 create mode 100644 roles/serveur_resolveur/README.md create mode 100644 roles/serveur_resolveur/defaults/main.yml create mode 100644 roles/serveur_resolveur/handlers/main.yml create mode 100644 roles/serveur_resolveur/meta/authentification.yml create mode 100644 roles/serveur_resolveur/meta/empreinte.yml create mode 100644 roles/serveur_resolveur/meta/flux.yml create mode 100644 roles/serveur_resolveur/tasks/main.yml create mode 100644 roles/serveur_resolveur/templates/setops.conf.j2 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*.