Set-OPS-Public/roles/serveur_powerdns/tasks/zones-publiques.yml
Daniel Allaire 19bbadfb11 DNS public en production : cinq fautes que seule la production montrait
Applicateur de frontiere : ecrire n est pas charger, le chemin rien a faire charge
desormais. site-verifier compare enfin playbooks/site.yml, regenere avec le groupe.
Base TSIG dans un repertoire a pdns et pdnsutil en pdns (journal SQLite). Flux UDP 5300 :
le secondaire demande le SOA avant de transferer, garde dans P82. socket-dir de
pdns@public, et le failed_when qui taisait la notification est retire. Eprouve sur les
machines : 2 zones sur 2 tirees automatiquement, NOTIFY compte, zone interne refusee.

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

276 lines
11 KiB
YAML

---
# --- LES ZONES PUBLIQUES DU LOCATAIRE : `autorite: primaire-cache` ---------------------
#
# Le locataire ECRIT ses zones publiques ; le serveur DNS public de son site les SERT.
# Ce fichier fait exercer enfin un mode que le plan declarait depuis longtemps et que rien
# ne consommait — « une autorite qu'on s'attribue sans l'exercer est une panne differee ».
#
# UNE INSTANCE A PART, ET C'EST LE COEUR DE LA CONCEPTION (2026-09-16).
#
# Pour que le site tire la zone, l'autoritatif doit ecouter sur l'adresse de la machine.
# Or l'instance principale sert AUSSI la zone `.internal`. Lui faire ecouter l'adresse de
# l'hote aurait permis au serveur public du site d'INTERROGER la zone interne du locataire
# — de lire la carte de ses machines. C'est precisement ce que
# `docs/responsabilites-locataire-hebergeur.md` refuse a l'hebergeur.
#
# `pdns@public` ne sert que les zones publiques et est la seule a ecouter sur l'hote.
# L'instance interne reste sur la boucle locale, sans une ligne changee.
- name: Retenir les zones publiques du plan
ansible.builtin.set_fact:
serveur_powerdns_zones_publiques: >-
{{ (domaines_publics | default({})) | dict2items
| selectattr('value.autorite', 'defined')
| selectattr('value.autorite', 'equalto', 'primaire-cache')
| map(attribute='key') | sort | list }}
- name: Refuser une publication incohérente
ansible.builtin.assert:
that:
# UNE ZONE INTERNE N'EST JAMAIS PUBLIQUE, meme si quelqu'un la declare
# `primaire-cache` par erreur.
- >-
(serveur_powerdns_zones_publiques | select('search', '\.internal\.?$') | list
| length) == 0
# L'ADRESSE PUBLIQUE EST PUBLIQUE. Une adresse privee dans une zone servie a Internet
# ne resout pour personne dehors, et renseigne sur le reseau dedans.
- serveur_powerdns_ip_publique | length > 0
- not (serveur_powerdns_ip_publique is search('^(10\.|127\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.)'))
# TSIG N'EST PAS DECORATIF. PowerDNS donne la zone a toute adresse de
# `allow-axfr-ips` SANS signature — mesure le 2026-09-16. Le serveur public du site
# ne doit donc jamais y figurer.
- serveur_powerdns_dns_public_site not in (serveur_powerdns_allow_axfr_ips | default([]))
- (vault_tsig_dns_public | default('')) | length > 0
fail_msg: >-
Zones publiques refusees : zone `.internal` declaree publique, adresse publique
absente ou privee ({{ serveur_powerdns_ip_publique | default('?') }}), serveur public
du site liste dans `allow-axfr-ips` (TSIG deviendrait decoratif), ou
`vault_tsig_dns_public` absent de la voute.
quiet: true
when: serveur_powerdns_zones_publiques | length > 0
- name: Créer le répertoire des zones publiques
ansible.builtin.file:
path: "{{ serveur_powerdns_public_zones_dir }}"
state: directory
owner: root
group: pdns
mode: "0750"
when: serveur_powerdns_zones_publiques | length > 0
# --- LE SERIAL SUIT LE CONTENU, PAS LE DEPLOIEMENT -----------------------------------
#
# LE PIEGE (2026-09-16). La zone interne porte un serial FIGE (`serveur_powerdns_serial`).
# Sans secondaire, c'est sans consequence. Avec un secondaire, c'est fatal en silence : il
# compare les serials, ne voit jamais de changement, et garde l'ancienne zone POUR
# TOUJOURS — pendant que le primaire sert la nouvelle.
#
# On rend donc d'abord le CORPS de la zone, sans serial. Si le corps change, le serial
# prend l'heure (secondes Unix : monotone, et tient dans 32 bits jusqu'en 2106). Sinon il
# garde sa valeur. Un deploiement sans changement ne notifie personne.
- name: Rendre le corps de chaque zone publique
ansible.builtin.template:
src: zone-publique.db.j2
dest: "{{ serveur_powerdns_public_zones_dir }}/{{ zone_publique }}.corps"
owner: root
group: pdns
mode: "0640"
vars:
serveur_powerdns_serial_public: "SERIAL"
loop: "{{ serveur_powerdns_zones_publiques }}"
loop_control:
loop_var: zone_publique
register: serveur_powerdns_corps_publics
- name: Relever le serial en place de chaque zone publique
ansible.builtin.slurp:
src: "{{ serveur_powerdns_public_zones_dir }}/{{ zone_publique }}.serial"
loop: "{{ serveur_powerdns_zones_publiques }}"
loop_control:
loop_var: zone_publique
register: serveur_powerdns_serials_publics
failed_when: false
# PAS UN HANDLER, ET C'EST VOULU : le serial doit avancer AVANT que la zone soit rendue,
# dans la tache suivante. Un handler s'executerait a la fin du jeu — la zone serait deja
# ecrite avec l'ancien serial, et le secondaire ne verrait jamais le changement.
- name: Avancer le serial des zones dont le contenu a changé # noqa: no-handler
ansible.builtin.copy:
content: "{{ ansible_date_time.epoch }}\n"
dest: "{{ serveur_powerdns_public_zones_dir }}/{{ item.zone_publique }}.serial"
owner: root
group: pdns
mode: "0640"
loop: "{{ serveur_powerdns_corps_publics.results | default([]) }}"
loop_control:
label: "{{ item.zone_publique }}"
when: item.changed | default(false)
register: serveur_powerdns_serials_avances
- name: Écrire chaque zone publique avec son serial
ansible.builtin.template:
src: zone-publique.db.j2
dest: "{{ serveur_powerdns_public_zones_dir }}/{{ zone_publique }}.zone"
owner: root
group: pdns
mode: "0640"
vars:
_avance: >-
{{ (serveur_powerdns_serials_avances.results | default([])
| selectattr('item.zone_publique', 'equalto', zone_publique)
| selectattr('changed') | list | length) > 0 }}
_ancien: >-
{{ (serveur_powerdns_serials_publics.results | default([])
| selectattr('zone_publique', 'equalto', zone_publique)
| map(attribute='content') | first | default('') | b64decode | trim) }}
serveur_powerdns_serial_public: >-
{{ ansible_date_time.epoch if (_avance | bool) or not _ancien else _ancien }}
loop: "{{ serveur_powerdns_zones_publiques }}"
loop_control:
loop_var: zone_publique
notify: Valider et redemarrer le DNS public du locataire
# --- L'INSTANCE ---------------------------------------------------------------------
- name: Déployer la configuration de l'instance publique
ansible.builtin.template:
src: pdns-public.conf.j2
dest: /etc/powerdns/pdns-public.conf
owner: root
group: pdns
mode: "0640"
when: serveur_powerdns_zones_publiques | length > 0
notify: Valider et redemarrer le DNS public du locataire
- name: Déployer l'index des zones publiques
ansible.builtin.template:
src: bindbackend-public.conf.j2
dest: "{{ serveur_powerdns_public_bind_config }}"
owner: root
group: pdns
mode: "0640"
when: serveur_powerdns_zones_publiques | length > 0
notify: Valider et redemarrer le DNS public du locataire
# LA BASE APPARTIENT A PDNS, ET SON REPERTOIRE AUSSI (mesure le 2026-09-16, en production).
#
# La premiere version posait la base dans `/var/lib/powerdns`, qui appartient a root. Le
# fichier etait bien a pdns — mais SQLite ecrit son JOURNAL a cote du fichier, dans le
# repertoire. `pdns@public` refusait donc de demarrer : « attempt to write a readonly
# database ». L'epreuve de l'outil ne pouvait pas le voir : elle tournait dans un /tmp qui
# appartenait a l'utilisateur qui la lancait.
#
# Toutes les commandes `pdnsutil` s'executent donc en `pdns`. Lancees en root, elles
# laisseraient un journal appartenant a root au premier incident — et la meme panne
# reviendrait, plus tard, sans cause visible.
- name: Créer le répertoire de la base, à pdns
ansible.builtin.file:
path: "{{ serveur_powerdns_public_dnssec_db | dirname }}"
state: directory
owner: pdns
group: pdns
mode: "0750"
when: serveur_powerdns_zones_publiques | length > 0
# L'ANCIEN EMPLACEMENT PORTE UNE CLE TSIG. Le laisser serait garder un secret a un endroit
# que plus rien ne lit ni ne surveille.
- name: Retirer la base de l'ancien emplacement
ansible.builtin.file:
path: /var/lib/powerdns/public-dnssec.db
state: absent
- name: Créer la base des clés TSIG de l'instance publique
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
cmd: >-
pdnsutil --config-name=public create-bind-db
{{ serveur_powerdns_public_dnssec_db }}
creates: "{{ serveur_powerdns_public_dnssec_db }}"
when:
- serveur_powerdns_zones_publiques | length > 0
- not ansible_check_mode
- name: Confier la base des clés à pdns
ansible.builtin.file:
path: "{{ serveur_powerdns_public_dnssec_db }}"
owner: pdns
group: pdns
mode: "0640"
when:
- serveur_powerdns_zones_publiques | length > 0
- not ansible_check_mode
# --- TSIG : SEULE LA CLE OUVRE LE TRANSFERT ------------------------------------------
- name: Relever les clés TSIG de l'instance publique
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
cmd: pdnsutil --config-name=public list-tsig-keys
register: serveur_powerdns_cles_publiques
changed_when: false
no_log: true
when:
- serveur_powerdns_zones_publiques | length > 0
- not ansible_check_mode
# On COMPARE avant d'ecrire : un secret change dans la voute doit remplacer l'ancien.
- name: Poser ou remplacer la clé TSIG de la relation avec le site
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
argv:
- pdnsutil
- --config-name=public
- import-tsig-key
- "{{ serveur_powerdns_tsig_cle }}"
- hmac-sha256
- "{{ vault_tsig_dns_public }}"
when:
- serveur_powerdns_zones_publiques | length > 0
- not ansible_check_mode
- vault_tsig_dns_public not in (serveur_powerdns_cles_publiques.stdout | default(''))
changed_when: true # n'arrive ici que si la cle manque ou differe
no_log: true
notify: Valider et redemarrer le DNS public du locataire
- name: Relever les autorisations de transfert
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
argv: [pdnsutil, --config-name=public, get-meta, "{{ zone_publique }}", TSIG-ALLOW-AXFR]
loop: "{{ serveur_powerdns_zones_publiques }}"
loop_control:
loop_var: zone_publique
register: serveur_powerdns_meta_publiques
changed_when: false
failed_when: false
when: not ansible_check_mode
- name: N'autoriser le transfert qu'à la clé de la relation
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
argv:
- pdnsutil
- --config-name=public
- set-meta
- "{{ item.zone_publique }}"
- TSIG-ALLOW-AXFR
- "{{ serveur_powerdns_tsig_cle }}"
loop: "{{ serveur_powerdns_meta_publiques.results | default([]) }}"
loop_control:
label: "{{ item.zone_publique }}"
when:
- not ansible_check_mode
- ("= " ~ serveur_powerdns_tsig_cle) not in (item.stdout | default(''))
changed_when: true # n'arrive ici que si la metadonnee manque ou differe
- name: Activer et démarrer l'instance publique
ansible.builtin.systemd:
name: pdns@public
enabled: true
state: started
when:
- serveur_powerdns_zones_publiques | length > 0
- not ansible_check_mode