From fa945de5a7bca6eb59dd90d4524148f0aad1308a Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Fri, 3 Jul 2026 14:35:00 -0400 Subject: [PATCH] =?UTF-8?q?client=5Funbound=20:=20r=C3=A9solveur=20local?= =?UTF-8?q?=20(DNS=20dynamique)=20=E2=80=94=20=C3=A9prouv=C3=A9=20sur=20un?= =?UTF-8?q?=20n=C5=93ud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unbound par nœud (127.0.0.1) : stub-zone vers PowerDNS (interne) + récursion Internet (ou forward). Alternative dynamique au plancher /etc/hosts. Bascule resolv.conf protégée (apply+confirm) ET validée avant (Unbound doit résoudre interne + Internet, sinon pas de bascule → nœud jamais coupé). Sûr par défaut (apply=false). Prouvé sur data-sql-01 : dig @127.0.0.1 keycloak/id-sso-01 → PowerDNS, deb.debian.org → récursion, apt OK. Rollout flotte = opt-in par nœud. Note : OPNsense embarque Unbound → l'Unbound réseau pourra vivre sur l'appliance de bordure (nœud public, Étape B) ; ce rôle reste complémentaire. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 11 +++ playbooks/groupes/client_unbound.yml | 15 ++++ roles/client_unbound/defaults/main.yml | 24 ++++++ roles/client_unbound/handlers/main.yml | 6 ++ roles/client_unbound/tasks/main.yml | 75 +++++++++++++++++++ roles/client_unbound/templates/setops.conf.j2 | 25 +++++++ 6 files changed, 156 insertions(+) create mode 100644 playbooks/groupes/client_unbound.yml create mode 100644 roles/client_unbound/defaults/main.yml create mode 100644 roles/client_unbound/handlers/main.yml create mode 100644 roles/client_unbound/tasks/main.yml create mode 100644 roles/client_unbound/templates/setops.conf.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d1b9b..9ee767c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,17 @@ fermée : déclarer `expose` → **vhost + SAN cert + A PowerDNS + alias plancher**, tout dérivé. Bugs corrigés en chemin : `serveur_loki` (groupe `loki` manquant), `stat` sur cible→contrôle, `become` inutile sur le contrôle. +- **Rôle `client_unbound` — résolveur local (DNS dynamique) : éprouvé sur un nœud.** Unbound par + nœud (`127.0.0.1`) avec **stub-zone** vers l'autoritatif interne (PowerDNS) + **récursion** + Internet (ou forward via `client_unbound_transitaires`). Alternative *dynamique* au plancher + `/etc/hosts` statique, sans casser Internet. Bascule de `/etc/resolv.conf` **protégée** + (`client_unbound_apply` + `client_unbound_confirm`) **et validée AVANT** (Unbound doit résoudre + interne + Internet, sinon pas de bascule → nœud jamais coupé). **Prouvé sur data-sql-01** (rayon + d'impact minimal) : `dig @127.0.0.1 keycloak/id-sso-01.lab.chezlepro.internal` → PowerDNS, + `deb.debian.org` → récursion, `apt` OK. Rôle sûr par défaut (`apply: false` : installe Unbound + sans toucher au resolver). Rollout flotte = opt-in par nœud. **Note direction** : OPNsense + embarque Unbound → à terme, l'Unbound *réseau* peut vivre sur l'appliance de bordure (nœud + public, Étape B) ; le rôle par-nœud reste portable et complémentaire (cache local). - **Bindings — Phase 1 : résolveur de liens dans `instancier.py` (relations service→service déclaratives).** Une application déclare ses `liens: [{vers, role}]` dans `plan/applications.yml` ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml` diff --git a/playbooks/groupes/client_unbound.yml b/playbooks/groupes/client_unbound.yml new file mode 100644 index 0000000..8a3ff1f --- /dev/null +++ b/playbooks/groupes/client_unbound.yml @@ -0,0 +1,15 @@ +--- +- name: Appliquer le groupe client_unbound + hosts: client_unbound + 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_unbound diff --git a/roles/client_unbound/defaults/main.yml b/roles/client_unbound/defaults/main.yml new file mode 100644 index 0000000..308ab35 --- /dev/null +++ b/roles/client_unbound/defaults/main.yml @@ -0,0 +1,24 @@ +--- +# 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), comme client_dns. +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" diff --git a/roles/client_unbound/handlers/main.yml b/roles/client_unbound/handlers/main.yml new file mode 100644 index 0000000..7df4880 --- /dev/null +++ b/roles/client_unbound/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Redémarrer unbound + ansible.builtin.systemd: + name: "{{ client_unbound_service }}" + state: restarted + when: not ansible_check_mode diff --git a/roles/client_unbound/tasks/main.yml b/roles/client_unbound/tasks/main.yml new file mode 100644 index 0000000..63daadf --- /dev/null +++ b/roles/client_unbound/tasks/main.yml @@ -0,0 +1,75 @@ +--- +- 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 + +# --- 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 new file mode 100644 index 0000000..eb4eeca --- /dev/null +++ b/roles/client_unbound/templates/setops.conf.j2 @@ -0,0 +1,25 @@ +# 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 %}