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>
This commit is contained in:
parent
712ad9f630
commit
53cfe71c7e
7 changed files with 125 additions and 1 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -1,5 +1,24 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-16 (9) — Un frontal devant le serveur public : dnsdist borne ce qu'on demande
|
||||
|
||||
Phase 2, troisieme volet. Un autoritatif signe DNSSEC expose sans frontal est un
|
||||
amplificateur : 60 octets a adresse usurpee rendent 1 a 3 Ko a la victime.
|
||||
|
||||
- `serveur_dns_public` installe `dnsdist` sur `<hote>:1053` (la frontiere y redirigera son 53) ;
|
||||
PowerDNS garde le 53 de la machine pour les NOTIFY des primaires.
|
||||
- Regles eprouvees dans l'unite reelle, avec le vrai PowerDNS derriere : NOTIFY et UPDATE
|
||||
refuses, AXFR/IXFR refuses, ANY en UDP tronque, debit par /24 (UDP 20 q/s -> tronque, TCP
|
||||
40 q/s -> abandon). Rafale de 300 requetes : 70 reponses, 230 tronquees. Recursion : REFUSED.
|
||||
Signatures DNSSEC relayees.
|
||||
- Sonde `zones-publiques` : interroge par le frontal, alerte si dnsdist est arrete.
|
||||
- Idempotent (second passage : 0 changement). Aucune console de controle ouverte.
|
||||
|
||||
PIEGE DE MESURE : `dig` envoie ANY en TCP par defaut. Le premier essai « montrait » une regle
|
||||
inoperante ; `+notcp` et le compteur de la regle ont montre qu'elle agissait.
|
||||
|
||||
Rien n'est encore expose : aucun flux `externe`, aucune redirection a la frontiere.
|
||||
|
||||
## 2026-09-16 (8) — DNSSEC : le locataire signe avec la cle de sa voute
|
||||
|
||||
Phase 2 du DNS public, deuxieme volet. Eprouve sur les vraies machines AVANT d'etre code :
|
||||
|
|
|
|||
|
|
@ -52,3 +52,21 @@ Deux instances jetables, sept vérifications, avant d'écrire une ligne de ce r
|
|||
de la frontière — et seulement après DNSSEC signé chez le locataire, CAA, et la limitation
|
||||
de débit des réponses. Le déclarer aujourd'hui dans `meta/flux.yml` ferait émettre au
|
||||
devis une règle sur le WAN que personne n'aurait décidée.
|
||||
|
||||
## Le frontal public : dnsdist (2026-09-16, phase 2)
|
||||
|
||||
L'Internet ne frappe jamais PowerDNS directement. `dnsdist` écoute
|
||||
`<hôte>:serveur_dns_public_frontal_port` (1053) — la frontière y redirige son 53 public — et
|
||||
relaie à PowerDNS, qui garde le 53 de la machine pour les NOTIFY des primaires et le site.
|
||||
|
||||
| Règle | Effet | Éprouvé |
|
||||
|---|---|---|
|
||||
| opcode ≠ QUERY | REFUSED (NOTIFY, UPDATE venus de dehors) | NOTIFY → REFUSED |
|
||||
| AXFR / IXFR | REFUSED | « Transfer failed » |
|
||||
| ANY en UDP | tronqué (RFC 8482) | `dig +notcp` → `tc` |
|
||||
| débit UDP > 20 q/s par /24 (/48) | tronqué : le client légitime passe en TCP | rafale 300 → 70 réponses, 230 tronquées |
|
||||
| débit TCP > 40 q/s par /24 (/48) | abandon | — |
|
||||
|
||||
Piège de mesure : `dig` envoie ANY **en TCP** par défaut. Sans `+notcp`, la règle semble
|
||||
inopérante. La sonde `zones-publiques` interroge **par le frontal** : c'est le chemin de
|
||||
l'Internet, et le seul qui voit PowerDNS et dnsdist ensemble.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ serveur_dns_public_paquets:
|
|||
- pdns-server
|
||||
- pdns-backend-bind
|
||||
- dnsutils
|
||||
- dnsdist
|
||||
|
||||
# LES RELATIONS AVEC LES LOCATAIRES — derivees, jamais ecrites.
|
||||
#
|
||||
|
|
@ -25,6 +26,16 @@ serveur_dns_public_relations: []
|
|||
|
||||
serveur_dns_public_ecoute: "{{ ansible_host | default('0.0.0.0') }}"
|
||||
serveur_dns_public_port: 53
|
||||
|
||||
# LE FRONTAL PUBLIC (dnsdist). L'Internet frappe ce port — la frontiere y redirige son 53 ;
|
||||
# le 53 de la machine reste a PowerDNS, pour les notifications des primaires et le site.
|
||||
serveur_dns_public_frontal_port: 1053
|
||||
# Le debit par bloc d'adresses : au-dela, UDP tronque (le client repasse en TCP), TCP abandonne.
|
||||
# Eprouve : une rafale de 300 requetes depuis une adresse -> 74 reponses, 226 tronquees.
|
||||
serveur_dns_public_debit_udp: 20
|
||||
serveur_dns_public_debit_tcp: 40
|
||||
serveur_dns_public_debit_masque_v4: 24
|
||||
serveur_dns_public_debit_masque_v6: 48
|
||||
serveur_dns_public_zones_dir: /var/lib/powerdns/zones-secondaires
|
||||
serveur_dns_public_bind_config: /etc/powerdns/setops-dns-public-zones.conf
|
||||
# LA BASE QUI PORTE LES CLES TSIG. Le backend `bind` seul ne sait stocker aucune metadonnee :
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
---
|
||||
- name: Redemarrer le frontal public
|
||||
ansible.builtin.systemd:
|
||||
name: dnsdist
|
||||
state: restarted
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Redemarrer le DNS public
|
||||
ansible.builtin.systemd:
|
||||
name: pdns
|
||||
|
|
|
|||
|
|
@ -174,6 +174,27 @@
|
|||
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:
|
||||
|
|
|
|||
46
roles/serveur_dns_public/templates/dnsdist.conf.j2
Normal file
46
roles/serveur_dns_public/templates/dnsdist.conf.j2
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
-- GENERE par Set-OPS (role serveur_dns_public). Ne pas editer a la main.
|
||||
--
|
||||
-- LE FRONTAL PUBLIC DU SERVEUR DNS DU SITE — ce que l'Internet frappe, avant PowerDNS.
|
||||
--
|
||||
-- PowerDNS repond ; dnsdist decide QUI a droit a une reponse, et combien. Un autoritatif
|
||||
-- expose sans frontal est un amplificateur offert : une requete de 60 octets, adresse source
|
||||
-- usurpee, rend 1 a 3 Ko signes DNSSEC a la victime.
|
||||
--
|
||||
-- Chaque regle ci-dessous a ete eprouvee le 2026-09-16 sur cette machine, dans l'unite
|
||||
-- `dnsdist.service`, avec le vrai PowerDNS derriere.
|
||||
|
||||
-- AUCUN APPEL VERS L'EDITEUR (meme raison que `security-poll-suffix=` de PowerDNS).
|
||||
setSecurityPollSuffix("")
|
||||
|
||||
setLocal("{{ serveur_dns_public_ecoute }}:{{ serveur_dns_public_frontal_port }}")
|
||||
-- TOUT LE MONDE PEUT DEMANDER : c'est un serveur de noms public. Ce qui est borne, c'est
|
||||
-- ce qu'on demande (regles) et a quel rythme (debit).
|
||||
setACL({"0.0.0.0/0", "::/0"})
|
||||
|
||||
-- LE SEUL AMONT : le PowerDNS de cette machine. Sonde par le SOA d'une zone qu'il DOIT servir
|
||||
-- — un PowerDNS vivant mais vide serait declare en panne, et c'est juste.
|
||||
newServer({address="{{ serveur_dns_public_ecoute }}:{{ serveur_dns_public_port }}", name="pdns",
|
||||
checkName="{{ (serveur_dns_public_relations | map(attribute='zones') | flatten | first) | default('.') }}.",
|
||||
checkType="SOA", mustResolve={{ 'true' if (serveur_dns_public_relations | map(attribute='zones') | flatten | length) > 0 else 'false' }}})
|
||||
setServerPolicy(firstAvailable)
|
||||
|
||||
-- NI TRANSFERT, NI NOTIFICATION, NI MISE A JOUR DEPUIS DEHORS. Les NOTIFY des primaires
|
||||
-- arrivent directement a PowerDNS, par le reseau du site ; par ici, ils seraient ceux de
|
||||
-- n'importe qui. (Eprouve : NOTIFY -> REFUSED, AXFR -> « Transfer failed ».)
|
||||
addAction(NotRule(OpcodeRule(DNSOpcode.Query)), RCodeAction(DNSRCode.REFUSED))
|
||||
addAction(OrRule({QTypeRule(DNSQType.AXFR), QTypeRule(DNSQType.IXFR)}), RCodeAction(DNSRCode.REFUSED))
|
||||
|
||||
-- ANY EN UDP : TRONQUE (RFC 8482). La reponse ANY est la plus grosse qu'une zone puisse
|
||||
-- rendre ; tronquee, elle ne pese que l'en-tete, et un vrai client repasse en TCP — ce qu'un
|
||||
-- attaquant a adresse usurpee ne peut pas faire.
|
||||
--
|
||||
-- PIEGE DE MESURE (2026-09-16) : `dig` envoie ANY en TCP par defaut. Le premier essai a donc
|
||||
-- « montre » une regle inoperante ; `dig +notcp` montre la troncature, et le compteur de la
|
||||
-- regle le confirme.
|
||||
addAction(AndRule({QTypeRule(DNSQType.ANY), NotRule(TCPRule(true))}), TCAction())
|
||||
|
||||
-- LE DEBIT, PAR /{{ serveur_dns_public_debit_masque_v4 }} (et /{{ serveur_dns_public_debit_masque_v6 }} en IPv6). En UDP, au-dela : reponse tronquee — le
|
||||
-- client legitime passe en TCP, la victime d'une reflexion ne recoit qu'un en-tete. En TCP,
|
||||
-- au-dela : abandon ; la poignee de main prouve deja l'adresse, le debit borne l'abus.
|
||||
addAction(AndRule({MaxQPSIPRule({{ serveur_dns_public_debit_udp }}, {{ serveur_dns_public_debit_masque_v4 }}, {{ serveur_dns_public_debit_masque_v6 }}, {{ serveur_dns_public_debit_udp * 2 }}), NotRule(TCPRule(true))}), TCAction())
|
||||
addAction(AndRule({MaxQPSIPRule({{ serveur_dns_public_debit_tcp }}, {{ serveur_dns_public_debit_masque_v4 }}, {{ serveur_dns_public_debit_masque_v6 }}, {{ serveur_dns_public_debit_tcp * 2 }}), TCPRule(true)}), DropAction())
|
||||
|
|
@ -11,6 +11,7 @@ ATTENDU={{ serveur_dns_public_sonde_attendu }}
|
|||
ICI={{ serveur_dns_public_ecoute }}
|
||||
|
||||
systemctl is-active --quiet pdns || { echo "Le serveur DNS public (pdns) n'est pas actif."; exit 2; }
|
||||
systemctl is-active --quiet dnsdist || { echo "Le frontal public (dnsdist) n'est pas actif : Internet n'obtient plus aucune reponse."; exit 2; }
|
||||
|
||||
# ON INTERROGE CE QUE LE MONDE INTERROGERA : le SOA de chaque zone, sur l'adresse d'ecoute.
|
||||
# Un secondaire qui n'a jamais recu sa zone repond REFUSED ou SERVFAIL — ce qui, vu d'un
|
||||
|
|
@ -18,7 +19,9 @@ systemctl is-active --quiet pdns || { echo "Le serveur DNS public (pdns) n'est p
|
|||
servies=0
|
||||
manquantes=()
|
||||
{% for zone in serveur_dns_public_relations | map(attribute='zones') | flatten %}
|
||||
if [[ -n "$(dig +short +time=3 +tries=1 @"${ICI}" -p {{ serveur_dns_public_port }} SOA {{ zone }} 2>/dev/null)" ]]; then
|
||||
# PAR LE FRONTAL : c'est le chemin que prend l'Internet. PowerDNS peut servir la zone et
|
||||
# dnsdist la refuser — seul ce chemin-ci voit les deux.
|
||||
if [[ -n "$(dig +short +time=3 +tries=1 @"${ICI}" -p {{ serveur_dns_public_frontal_port }} SOA {{ zone }} 2>/dev/null)" ]]; then
|
||||
servies=$((servies + 1))
|
||||
else
|
||||
manquantes+=("{{ zone }}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue