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>
213 lines
7.7 KiB
YAML
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"
|