Set-OPS-Public/roles/serveur_dns_public/tasks/main.yml
Daniel Allaire 53cfe71c7e DNS public : dnsdist devant PowerDNS, debit et refus eprouves
ANY UDP tronque, AXFR/NOTIFY/UPDATE refuses depuis dehors, debit par /24 (rafale 300 ->
70 reponses, 230 tronquees). PowerDNS garde le 53 pour les primaires ; le frontal ecoute
1053, ou la frontiere redirigera. Sonde par le frontal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:29:19 -04:00

213 lines
7.7 KiB
YAML

---
# --- LES REFUS, AVANT DE TOUCHER A QUOI QUE CE SOIT -----------------------------------
# UNE ZONE INTERNE N'ARRIVE JAMAIS ICI. Ce serveur repond a Internet : une zone `.internal`
# servie ici serait la carte des machines d'un locataire offerte au monde. La derivation ne
# retient que `primaire-cache` ; ce refus garde le cas ou elle se tromperait.
- name: Refuser une zone interne sur le serveur public
ansible.builtin.assert:
that:
- >-
(serveur_dns_public_relations | map(attribute='zones') | flatten
| select('search', '\.internal\.?$') | list | length) == 0
fail_msg: >-
Une zone `.internal` est destinee au serveur DNS PUBLIC. Elle publierait la carte
des machines d'un locataire. Seules les zones `autorite: primaire-cache` y ont leur
place.
quiet: true
# UNE RELATION SANS SECRET N'EST PAS UNE RELATION NON SIGNEE : C'EST UN REFUS. Un
# secondaire qui tirerait sans cle ne recevrait rien (le primaire n'autorise aucune
# adresse) — ou pire, recevrait tout le jour ou quelqu'un ajouterait son adresse a
# `allow-axfr-ips`, et TSIG deviendrait decoratif sans le moindre message.
- name: Exiger un secret TSIG pour chaque relation
ansible.builtin.assert:
that:
- lookup('vars', item.secret_var, default='') | length > 0
fail_msg: >-
{{ item.ecosysteme }} : `{{ item.secret_var }}` est absent de la voute du site. Sans
lui, la zone de ce locataire ne se transfere pas — et aucun repli non signe n'est
prevu, par construction.
quiet: true
loop: "{{ serveur_dns_public_relations }}"
loop_control:
label: "{{ item.ecosysteme }}"
# --- L'INSTALLATION -------------------------------------------------------------------
- name: Installer PowerDNS Authoritative
ansible.builtin.apt:
name: "{{ serveur_dns_public_paquets }}"
state: present
update_cache: true
cache_valid_time: 3600
# Le secondaire ECRIT les zones qu'il recoit : le repertoire appartient a `pdns`.
- name: Créer le répertoire des zones secondaires
ansible.builtin.file:
path: "{{ serveur_dns_public_zones_dir }}"
state: directory
owner: pdns
group: pdns
mode: "0750"
- name: Déployer la configuration du serveur public
ansible.builtin.template:
src: setops-dns-public.conf.j2
dest: /etc/powerdns/pdns.d/setops-dns-public.conf
owner: root
group: pdns
mode: "0640"
notify: Redemarrer le DNS public
- name: Déployer l'index des zones secondaires
ansible.builtin.template:
src: bindbackend.conf.j2
dest: "{{ serveur_dns_public_bind_config }}"
owner: root
group: pdns
mode: "0640"
notify: Redemarrer le DNS public
# 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_dns_public_dnssec_db | dirname }}"
state: directory
owner: pdns
group: pdns
mode: "0750"
- name: Créer la base des clés TSIG
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
cmd: "pdnsutil create-bind-db {{ serveur_dns_public_dnssec_db }}"
creates: "{{ serveur_dns_public_dnssec_db }}"
when: not ansible_check_mode
- name: Confier la base des clés à pdns
ansible.builtin.file:
path: "{{ serveur_dns_public_dnssec_db }}"
owner: pdns
group: pdns
mode: "0640"
when: not ansible_check_mode
# --- LES CLES TSIG, UNE PAR RELATION --------------------------------------------------
#
# On COMPARE avant d'ecrire, et c'est ce qui rend la rotation possible : un secret change
# dans la voute doit remplacer l'ancien. Importer seulement quand le nom manque laisserait
# la vieille cle en place pour toujours, et le transfert echouerait sans rien dire.
- name: Relever les clés TSIG en place
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
cmd: pdnsutil list-tsig-keys
register: serveur_dns_public_cles
changed_when: false
no_log: true
when: not ansible_check_mode
- name: Poser ou remplacer la clé TSIG de chaque relation
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
argv:
- pdnsutil
- import-tsig-key
- "{{ item.cle_tsig }}"
- hmac-sha256
- "{{ lookup('vars', item.secret_var) }}"
loop: "{{ serveur_dns_public_relations }}"
loop_control:
label: "{{ item.ecosysteme }} → {{ item.cle_tsig }}"
when:
- not ansible_check_mode
- (item.cle_tsig ~ '. hmac-sha256. ' ~ lookup('vars', item.secret_var))
not in (serveur_dns_public_cles.stdout | default(''))
changed_when: true # n'arrive ici que si la cle manque ou differe
no_log: true
notify: Redemarrer le DNS public
- name: Relever les métadonnées de transfert
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
argv: [pdnsutil, get-meta, "{{ item.1 }}", AXFR-MASTER-TSIG]
loop: "{{ serveur_dns_public_relations | subelements('zones') }}"
loop_control:
label: "{{ item.1 }}"
register: serveur_dns_public_meta
changed_when: false
failed_when: false
when: not ansible_check_mode
- name: Exiger la signature TSIG sur chaque transfert entrant
become: true
become_user: pdns # voir « la base appartient a pdns »
ansible.builtin.command:
argv: [pdnsutil, set-meta, "{{ item.item.1 }}", AXFR-MASTER-TSIG, "{{ item.item.0.cle_tsig }}"]
loop: "{{ serveur_dns_public_meta.results | default([]) }}"
loop_control:
label: "{{ item.item.1 }} ← {{ item.item.0.cle_tsig }}"
when:
- not ansible_check_mode
- ("= " ~ item.item.0.cle_tsig) not in (item.stdout | default(''))
changed_when: true # n'arrive ici que si la metadonnee manque ou differe
notify: Redemarrer le DNS public
- name: Activer et démarrer PowerDNS
ansible.builtin.systemd:
name: pdns
enabled: true
state: started
when: not ansible_check_mode
# --- LE FRONTAL PUBLIC (dnsdist) -----------------------------------------------------
#
# Ce que l'Internet frappe. PowerDNS garde le 53 de la machine pour les notifications des
# primaires ; la frontiere redirige son 53 public vers ce frontal, jamais vers PowerDNS.
- name: Déployer la configuration du frontal public
ansible.builtin.template:
src: dnsdist.conf.j2
dest: /etc/dnsdist/dnsdist.conf
owner: root
group: _dnsdist
mode: "0640"
validate: dnsdist --check-config -C %s
notify: Redemarrer le frontal public
- name: Activer et démarrer le frontal public
ansible.builtin.systemd:
name: dnsdist
enabled: true
state: started
when: not ansible_check_mode
# --- SONDE (docs/supervision-conception.md) -------------------------------------------
- name: Assurer le répertoire des sondes de supervision
ansible.builtin.file:
path: /usr/local/lib/setops/sondes
state: directory
owner: root
group: root
mode: "0755"
- name: Déposer la sonde « zones-publiques »
ansible.builtin.template:
src: sonde-zones-publiques.sh.j2
dest: /usr/local/lib/setops/sondes/zones-publiques.sh
owner: root
group: root
mode: "0750"