dns : quatre zones inverses pour le site, et rien de plus
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
Le site ne servait aucun PTR. `serveur_powerdns_zone_inverse` derivait d'un supernet /16 — la forme d'un TENANT, qui tire tout de son index. Un site ne derive pas : il declare plusieurs /24 et n'a pas de supernet unique, si bien que la derivation rendait une chaine vide et qu'aucune zone n'etait generee. Le site revendique desormais exactement ce qu'il occupe : 31.0.10.in-addr.arpa 32.0.10.in-addr.arpa 33.0.10.in-addr.arpa 34.0.10.in-addr.arpa Revendiquer `0.10.in-addr.arpa` d'un seul geste aurait ete plus simple et faux : cette zone couvre aussi la frontiere, le transit et les hyperviseurs, qui ne sont pas a lui. Une autorite qu'on s'attribue sans l'exercer est une panne differee — le resolveur repondrait NXDOMAIN pour des adresses qu'un autre sait nommer. LA DERIVATION VIT DANS UN FILTRE (`zones_inverses`) parce qu'elle a DEUX appelants : `serveur_powerdns` ecrit ces zones, `serveur_resolveur` les delegue a l'autoritatif. Deux calculs separes finiraient par diverger, et la divergence ne se verrait qu'au premier PTR interroge. Le nom d'une zone dit sa profondeur — trois etiquettes numeriques valent un /24, deux valent un /16 — et le modele en deduit seul la forme du PTR. DEUX CHEMINS MORTS TROUVES EN ROUTE. Les zones etaient servies, et personne ne les demandait. `serveur_resolveur` deleguait la zone directe par une `stub-zone` mais pas les inverses : `dig -x` rendait vide depuis les cinq machines alors que la meme requete posee directement a l'autoritatif repondait juste. Un service correct derriere un chemin que rien n'emprunte. Puis, les stubs poses, Unbound repondait toujours NXDOMAIN avec le drapeau `aa` — une reponse AUTORITAIRE, sans jamais consulter le stub. Il embarque des `local-zone` pour tout l'espace RFC1918 inverse. `nodefault` n'y change rien : ce mode n'agit que si le nom correspond EXACTEMENT a une zone par defaut, et la sienne est `10.in-addr.arpa`, le /8 entier. C'est `transparent` qui laisse la requete suivre son cours — pour nos quatre zones seulement, la ou `unblock-lan-zones` aurait ouvert tout l'espace prive. Rien ne distinguait ce blocage d'une absence : le meme NXDOMAIN qu'un nom qui n'existe pas. AUSSI : les zones inverses sont desormais validees par `named-checkzone` comme la directe (un fichier mal forme etait refuse en silence par PowerDNS), et une zone qu'un ecosysteme cesse de revendiquer est retiree du repertoire. VERIFICATION. Les cinq PTR resolvent depuis les cinq hotes par le resolveur, les huit noms directs par le plancher ET par le DNS, et les deux roles sont idempotents (changed=0). P47 evalue la derivation sur cinq cas — un site a quatre zones, un tenant a une, deux machines d'un meme /24 qui n'en font qu'une, aucune adresse, une adresse illisible. Controle negatif verifie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a74e35bf21
commit
e5841ea684
12 changed files with 284 additions and 31 deletions
48
CHANGELOG.md
48
CHANGELOG.md
|
|
@ -1,5 +1,53 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-25 — Quatre zones inverses, et rien de plus
|
||||
|
||||
**47 preuves.** Le site ne servait aucun PTR. `serveur_powerdns_zone_inverse` dérivait
|
||||
d'un supernet `/16` — la forme d'un **tenant**, qui tire tout de son index. Un site ne
|
||||
dérive pas : il déclare plusieurs `/24` et n'a pas de supernet unique, si bien que la
|
||||
dérivation rendait une chaîne vide et qu'aucune zone n'était générée.
|
||||
|
||||
Le site revendique désormais **exactement ce qu'il occupe** :
|
||||
|
||||
```
|
||||
31.0.10.in-addr.arpa 32.0.10.in-addr.arpa
|
||||
33.0.10.in-addr.arpa 34.0.10.in-addr.arpa
|
||||
```
|
||||
|
||||
Revendiquer `0.10.in-addr.arpa` d'un seul geste aurait été plus simple et faux : cette
|
||||
zone couvre aussi la frontière, le transit et les hyperviseurs, qui ne sont pas à lui.
|
||||
*Une autorité qu'on s'attribue sans l'exercer est une panne différée.*
|
||||
|
||||
La dérivation vit dans un **filtre** (`zones_inverses`) parce qu'elle a deux appelants :
|
||||
`serveur_powerdns` écrit ces zones, `serveur_resolveur` les délègue à l'autoritatif. Deux
|
||||
calculs séparés finiraient par diverger, et la divergence ne se verrait qu'au premier PTR
|
||||
interrogé. Le nom d'une zone dit sa profondeur — trois étiquettes numériques valent un
|
||||
`/24`, deux valent un `/16` — et le modèle en déduit seul la forme du PTR.
|
||||
|
||||
### Deux chemins morts trouvés en route
|
||||
|
||||
Les zones étaient servies, et personne ne les demandait. `serveur_resolveur` déléguait la
|
||||
zone directe par une `stub-zone` mais pas les inverses : `dig -x` rendait vide depuis les
|
||||
cinq machines alors que la même requête posée directement à l'autoritatif répondait juste.
|
||||
*Un service correct derrière un chemin que rien n'emprunte.*
|
||||
|
||||
Puis, les stubs posés, Unbound répondait toujours **NXDOMAIN avec le drapeau `aa`** — une
|
||||
réponse *autoritaire*, sans jamais consulter le stub. Il embarque des `local-zone` pour
|
||||
tout l'espace RFC1918 inversé. `nodefault` n'y change rien : ce mode n'agit que si le nom
|
||||
correspond exactement à une zone par défaut, et la sienne est `10.in-addr.arpa`, le `/8`
|
||||
entier. C'est `transparent` qui laisse la requête suivre son cours — pour nos quatre zones
|
||||
seulement, là où `unblock-lan-zones` aurait ouvert tout l'espace privé.
|
||||
|
||||
Rien ne distinguait ce blocage d'une absence : le même NXDOMAIN qu'un nom qui n'existe pas.
|
||||
|
||||
### Vérification
|
||||
|
||||
Les cinq PTR résolvent depuis les cinq hôtes par le résolveur, les huit noms directs par
|
||||
le plancher **et** par le DNS, et les deux rôles sont idempotents (`changed=0`).
|
||||
|
||||
**P47** évalue la dérivation sur cinq cas — un site à quatre zones, un tenant à une, deux
|
||||
machines d'un même `/24` qui n'en font qu'une, aucune adresse, une adresse illisible.
|
||||
|
||||
## 2026-08-25 — Le plancher survit au redémarrage, et la zone dit les vraies adresses
|
||||
|
||||
**46 preuves.** Le découpage du site en quatre zones a déplacé cinq machines ; ni le
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/production/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (45 OK · 0 echec · 1 saute)
|
||||
- **Verdict** : ✅ CONFORME (46 OK · 0 echec · 1 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -59,6 +59,7 @@
|
|||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from inventory_rules import ( # noqa: E402
|
|||
bases_du_groupe,
|
||||
chaine_connexion,
|
||||
expositions_des_applications,
|
||||
zones_inverses,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -31,4 +32,5 @@ class FilterModule:
|
|||
"bases_du_groupe": bases_du_groupe,
|
||||
"chaine_connexion": chaine_connexion,
|
||||
"expositions_des_applications": expositions_des_applications,
|
||||
"zones_inverses": zones_inverses,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,16 +50,33 @@ serveur_powerdns_inventory_groups:
|
|||
serveur_powerdns_publier_expositions: true
|
||||
serveur_powerdns_expositions: []
|
||||
|
||||
# Zone INVERSE — DERIVEE du supernet, jamais ecrite. Un /16 `10.(10+index).0.0` donne
|
||||
# `(10+index).10.in-addr.arpa`. Vide si le supernet n'est pas connu ou n'est pas un /16 :
|
||||
# mieux vaut pas de zone inverse qu'une zone fausse.
|
||||
# ZONES INVERSES — DERIVEES, JAMAIS ECRITES.
|
||||
#
|
||||
# Pourquoi elle manquait. Rien dans le moteur ne touchait `in-addr.arpa` — mesure du
|
||||
# 2026-08-08. Un hote pouvait donc etre resolu par son nom et rester anonyme a l'envers,
|
||||
# ce que la plupart des services de courriel et de journalisation reprochent en silence.
|
||||
serveur_powerdns_zone_inverse: >-
|
||||
{{ (setops_supernet | default('') | ansible.utils.ipaddr('prefix') | int == 16)
|
||||
| ternary(
|
||||
(setops_supernet | default('0.0.0.0/0')).split('.')[1] ~ '.'
|
||||
~ (setops_supernet | default('0.0.0.0/0')).split('.')[0] ~ '.in-addr.arpa',
|
||||
'') }}
|
||||
# Rien dans le moteur ne touchait `in-addr.arpa` avant le 2026-08-08 : un hote pouvait
|
||||
# etre resolu par son nom et rester anonyme a l'envers, ce que le courriel et la
|
||||
# journalisation reprochent en silence.
|
||||
#
|
||||
# La premiere derivation supposait un supernet /16 — la forme d'un TENANT, qui tire tout
|
||||
# de son index. Un SITE ne derive pas : il declare ses reseaux, et il en declare
|
||||
# plusieurs. `setops_supernet` y vaut le /24 de la machine, jamais un /16, si bien que la
|
||||
# derivation rendait une chaine vide et qu'AUCUNE zone inverse n'etait generee (mesure du
|
||||
# 2026-08-25 : les cinq machines du site, sans PTR).
|
||||
#
|
||||
# On couvre desormais EXACTEMENT ce que l'ecosysteme occupe : un /16 quand il en a un,
|
||||
# sinon un /24 par reseau ou vit reellement une machine. Pour le site, les quatre zones
|
||||
# `31.0.10`, `32.0.10`, `33.0.10`, `34.0.10` — et rien au-dela. Revendiquer tout
|
||||
# `0.10.in-addr.arpa` aurait ete plus simple et faux : cette zone couvre aussi la
|
||||
# frontiere, le transit et les hyperviseurs, qui ne sont pas a lui. Une autorite qu'on
|
||||
# s'attribue sans l'exercer est une panne differee.
|
||||
#
|
||||
# LA DERIVATION VIT DANS UN FILTRE (`zones_inverses`), parce qu'elle a DEUX appelants :
|
||||
# `serveur_powerdns` ecrit ces zones, `serveur_resolveur` les delegue a l'autoritatif.
|
||||
# Deux calculs separes finiraient par diverger, et la divergence ne se verrait qu'au
|
||||
# premier PTR interroge.
|
||||
#
|
||||
# Le nom d'une zone dit sa profondeur : trois etiquettes numeriques valent un /24, deux
|
||||
# valent un /16. Le modele en deduit seul la forme du PTR ; rien n'est ecrit deux fois.
|
||||
serveur_powerdns_zones_inverses: >-
|
||||
{{ (serveur_powerdns_inventory_groups | map('extract', groups) | select | flatten
|
||||
| map('extract', hostvars, 'ansible_host') | select | list)
|
||||
| zones_inverses(setops_supernet | default('')) }}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,22 @@
|
|||
when: not ansible_check_mode # en dry-run le fichier de zone n'existe pas encore
|
||||
listen: Validate and reload PowerDNS
|
||||
|
||||
# UNE ZONE NON VALIDEE EST UNE ZONE QUI PEUT NE PAS ETRE SERVIE. La zone directe etait
|
||||
# verifiee, les inverses non : PowerDNS refuse silencieusement un fichier mal forme et
|
||||
# continue de servir le reste, si bien qu'un PTR casse ne se voit qu'a l'usage — et
|
||||
# l'usage, ici, c'est un journal ou un courriel qui reproche un nom sans le dire.
|
||||
- name: Validate PowerDNS reverse zones
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
named-checkzone {{ zone_inverse }}
|
||||
{{ serveur_powerdns_zone_directory }}/{{ zone_inverse }}.zone
|
||||
changed_when: false
|
||||
loop: "{{ serveur_powerdns_zones_inverses | default([]) }}"
|
||||
loop_control:
|
||||
loop_var: zone_inverse
|
||||
when: not ansible_check_mode # en dry-run le fichier de zone n'existe pas encore
|
||||
listen: Validate and reload PowerDNS
|
||||
|
||||
- name: Restart PowerDNS
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ serveur_powerdns_service_name }}"
|
||||
|
|
|
|||
|
|
@ -83,14 +83,38 @@
|
|||
|
||||
# Meme source que la zone directe : `hotes_actifs` et son `ansible_host`. Deux zones
|
||||
# alimentees par une seule verite — l'endroit ou elles divergeraient n'existe pas.
|
||||
- name: Générer la zone DNS inverse depuis l'inventaire
|
||||
- name: Générer les zones DNS inverses depuis l'inventaire
|
||||
ansible.builtin.template:
|
||||
src: zone-inverse.db.j2
|
||||
dest: "{{ serveur_powerdns_zone_directory }}/{{ serveur_powerdns_zone_inverse }}.zone"
|
||||
dest: "{{ serveur_powerdns_zone_directory }}/{{ zone_inverse }}.zone"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: serveur_powerdns_zone_inverse | length > 0
|
||||
loop: "{{ serveur_powerdns_zones_inverses | default([]) }}"
|
||||
loop_control:
|
||||
loop_var: zone_inverse
|
||||
notify: Validate and reload PowerDNS
|
||||
|
||||
# UNE ZONE INVERSE QUI N'EST PLUS REVENDIQUEE DOIT DISPARAITRE. Sans ce retrait, un
|
||||
# ecosysteme qui abandonne un reseau laisserait derriere lui un fichier que le backend ne
|
||||
# declare plus mais que le repertoire garde — et le prochain lecteur croirait la zone
|
||||
# servie.
|
||||
- name: Relever les zones inverses présentes sur la machine
|
||||
ansible.builtin.find:
|
||||
paths: "{{ serveur_powerdns_zone_directory }}"
|
||||
patterns: "*.in-addr.arpa.zone"
|
||||
register: serveur_powerdns_inverses_presentes
|
||||
|
||||
- name: Retirer les zones inverses que l'écosystème ne revendique plus
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ serveur_powerdns_inverses_presentes.files | default([]) }}"
|
||||
loop_control:
|
||||
label: "{{ item.path | basename }}"
|
||||
when: >-
|
||||
(item.path | basename | regex_replace('\.zone$', ''))
|
||||
not in (serveur_powerdns_zones_inverses | default([]))
|
||||
notify: Validate and reload PowerDNS
|
||||
|
||||
- name: Activer et démarrer PowerDNS
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ zone "{{ serveur_powerdns_zone }}" {
|
|||
type master;
|
||||
file "{{ serveur_powerdns_zone_directory }}/{{ serveur_powerdns_zone }}.zone";
|
||||
};
|
||||
{% if serveur_powerdns_zone_inverse %}
|
||||
zone "{{ serveur_powerdns_zone_inverse }}" {
|
||||
{# Une declaration par zone inverse : un ecosysteme qui occupe plusieurs reseaux en a
|
||||
plusieurs. Un tenant tire un /16 de son index et n'en a qu'une ; un site declare ses
|
||||
reseaux et en a une par reseau occupe. #}
|
||||
{% for zone_inverse in serveur_powerdns_zones_inverses | default([]) %}
|
||||
zone "{{ zone_inverse }}" {
|
||||
type master;
|
||||
file "{{ serveur_powerdns_zone_directory }}/{{ serveur_powerdns_zone_inverse }}.zone";
|
||||
file "{{ serveur_powerdns_zone_directory }}/{{ zone_inverse }}.zone";
|
||||
};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
; Zone INVERSE — generee par Set-OPS (role serveur_powerdns). Ne pas editer a la main.
|
||||
; Zone INVERSE {{ zone_inverse }} — generee par Set-OPS (role serveur_powerdns).
|
||||
; Ne pas editer a la main.
|
||||
;
|
||||
; Le nom de la zone est DERIVE du supernet du tenant, comme tout le reste : un /16
|
||||
; 10.(10+index).0.0 donne (10+index).10.in-addr.arpa. Rien n'est ecrit.
|
||||
; LE NOM DE LA ZONE DIT SA PROFONDEUR, et le PTR s'en deduit. Trois etiquettes numeriques
|
||||
; (`31.0.10.in-addr.arpa`) valent un /24 : le PTR ne porte que le dernier octet. Deux
|
||||
; (`27.10.in-addr.arpa`) valent un /16 : il en porte deux. Rien n'est ecrit — ni le
|
||||
; prefixe, ni la forme du nom.
|
||||
;
|
||||
; Les PTR viennent de la MEME source que les A — `hotes_actifs` et son `ansible_host`.
|
||||
; Deux zones alimentees par une seule verite : c'est ce qui empeche l'endroit ou elles
|
||||
; divergent d'exister.
|
||||
; Un TENANT tire un /16 de son index et n'a donc qu'une zone. Un SITE declare plusieurs
|
||||
; reseaux et en a une par reseau occupe. La meme derivation sert les deux.
|
||||
;
|
||||
; Les PTR viennent de la MEME source que les A — les groupes d'inventaire et leur
|
||||
; `ansible_host`. Deux zones alimentees par une seule verite : c'est ce qui empeche
|
||||
; l'endroit ou elles divergent d'exister.
|
||||
{% set _labels = zone_inverse | regex_replace('\.in-addr\.arpa\.?$', '') | split('.') %}
|
||||
{% set _prefixe = _labels | reverse | list %}
|
||||
$TTL {{ serveur_powerdns_ttl }}
|
||||
@ IN SOA {{ serveur_powerdns_nameserver }}.{{ serveur_powerdns_zone }}. {{ serveur_powerdns_contact }}. (
|
||||
{{ serveur_powerdns_serial }} ; serial
|
||||
|
|
@ -13,12 +21,14 @@ $TTL {{ serveur_powerdns_ttl }}
|
|||
{{ serveur_powerdns_soa_retry }} ; retry
|
||||
{{ serveur_powerdns_soa_expire }} ; expire
|
||||
{{ serveur_powerdns_soa_minimum }} ) ; minimum
|
||||
|
||||
@ IN NS {{ serveur_powerdns_nameserver }}.{{ serveur_powerdns_zone }}.
|
||||
|
||||
{% for host_name in groups.get('hotes_actifs', []) | sort %}
|
||||
{% set _vus = [] %}
|
||||
{% for _groupe in serveur_powerdns_inventory_groups %}
|
||||
{% for host_name in groups.get(_groupe, []) | sort if host_name not in _vus %}
|
||||
{% set ip = hostvars[host_name].ansible_host | default('') %}
|
||||
{% if ip %}
|
||||
{{ ip.split('.')[3] }}.{{ ip.split('.')[2] }} IN PTR {{ host_name }}.{{ serveur_powerdns_zone }}.
|
||||
{% if ip and (ip.split('.')[:_prefixe | length] == _prefixe) %}
|
||||
{% set _ = _vus.append(host_name) %}
|
||||
{{ ip.split('.')[_prefixe | length:] | reverse | join('.') }} IN PTR {{ host_name }}.{{ serveur_powerdns_zone }}.
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,23 @@ serveur_resolveur_zone_interne: "{{ domaine_interne }}"
|
|||
serveur_resolveur_autoritatif: "127.0.0.1"
|
||||
serveur_resolveur_autoritatif_port: 5300
|
||||
|
||||
# LE RESOLVEUR DELEGUE A L'AUTORITATIF CE QUE L'AUTORITATIF SERT (2026-08-25).
|
||||
#
|
||||
# La zone souveraine lui etait deleguee par une `stub-zone` ; les zones INVERSES ne
|
||||
# l'etaient pas. PowerDNS servait donc les PTR sur son port, et personne ne les lui
|
||||
# demandait : `dig -x` rendait vide depuis les cinq machines alors que la meme requete
|
||||
# posee directement a l'autoritatif repondait juste.
|
||||
#
|
||||
# C'est la panne que le depot connait deja sous une autre forme — un service correct
|
||||
# derriere un chemin que rien n'emprunte.
|
||||
#
|
||||
# MEME DERIVATION QUE `serveur_powerdns`, par le meme filtre : deux calculs separes
|
||||
# finiraient par diverger, et la divergence ne se verrait qu'au premier PTR interroge.
|
||||
serveur_resolveur_zones_inverses: >-
|
||||
{{ (groups.get('hotes_actifs', []) | map('extract', hostvars, 'ansible_host')
|
||||
| select | list)
|
||||
| zones_inverses(setops_supernet | default('')) }}
|
||||
|
||||
# AUCUN TRANSITAIRE : Unbound récurse depuis les serveurs racine. Poser un transitaire ici
|
||||
# reviendrait à confier chaque question de l'écosystème à un tiers — ce que la bascule du
|
||||
# 2026-08-23 avait justement supprimé.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,31 @@ server:
|
|||
do-ip6: no
|
||||
# La zone souveraine n'est pas signée : on ne la soumet pas à la validation DNSSEC.
|
||||
domain-insecure: "{{ serveur_resolveur_zone_interne }}"
|
||||
{% for zone_inverse in serveur_resolveur_zones_inverses | default([]) %}
|
||||
# Les zones inverses ne le sont pas davantage — et `in-addr.arpa`, LUI, est signé
|
||||
# publiquement. Sans cette ligne, la validation chercherait une chaîne de confiance
|
||||
# vers une délégation qui n'existe pas, et tous les PTR rendraient SERVFAIL.
|
||||
domain-insecure: "{{ zone_inverse }}"
|
||||
# UNBOUND BLOQUE L'INVERSE PRIVÉ PAR DÉFAUT (2026-08-25).
|
||||
#
|
||||
# Il embarque des `local-zone` pour tout l'espace RFC1918 inversé, et y répond
|
||||
# lui-même. Avec la `stub-zone` pourtant bien déclarée, `dig -x` rendait NXDOMAIN
|
||||
# avec le drapeau `aa` — une réponse AUTORITAIRE d'Unbound, qui n'avait jamais
|
||||
# interrogé l'autoritatif. Le même NXDOMAIN qu'un nom qui n'existe pas : rien ne
|
||||
# distinguait le blocage de l'absence.
|
||||
#
|
||||
# `transparent` laisse la requête suivre son cours — donc atteindre la `stub-zone`
|
||||
# ci-dessous — pour CETTE zone seulement.
|
||||
#
|
||||
# Pas `nodefault` : ce mode ne retire un blocage que si le nom correspond EXACTEMENT
|
||||
# à une zone par défaut d'Unbound. La sienne est `10.in-addr.arpa`, le /8 entier ; nos
|
||||
# `/24` n'y correspondent pas, et la ligne restait sans effet — mesuré, le NXDOMAIN
|
||||
# autoritaire persistait après rechargement.
|
||||
#
|
||||
# Pas `unblock-lan-zones: yes` non plus : il aurait débloqué tout l'espace privé
|
||||
# inversé, y compris ce que cet écosystème ne sert pas.
|
||||
local-zone: "{{ zone_inverse }}" transparent
|
||||
{% endfor %}
|
||||
# UNBOUND REFUSE D'INTERROGER UNE LOOPBACK PAR DÉFAUT (2026-08-24).
|
||||
#
|
||||
# `do-not-query-localhost` vaut `yes` d'origine — une protection contre les boucles.
|
||||
|
|
@ -27,6 +52,16 @@ server:
|
|||
stub-zone:
|
||||
name: "{{ serveur_resolveur_zone_interne }}"
|
||||
stub-addr: {{ serveur_resolveur_autoritatif }}@{{ serveur_resolveur_autoritatif_port }}
|
||||
{# LES ZONES INVERSES SE DELEGUENT COMME LA DIRECTE (2026-08-25). Sans ces stubs,
|
||||
l'autoritatif servait les PTR et personne ne les lui demandait : `dig -x` rendait vide
|
||||
depuis les cinq machines, alors que la meme requete posee directement a l'autoritatif
|
||||
repondait juste. Un service correct derriere un chemin que rien n'emprunte. #}
|
||||
{% for zone_inverse in serveur_resolveur_zones_inverses | default([]) %}
|
||||
|
||||
stub-zone:
|
||||
name: "{{ zone_inverse }}"
|
||||
stub-addr: {{ serveur_resolveur_autoritatif }}@{{ serveur_resolveur_autoritatif_port }}
|
||||
{% endfor %}
|
||||
{% if serveur_resolveur_transitaires %}
|
||||
|
||||
forward-zone:
|
||||
|
|
|
|||
|
|
@ -519,6 +519,37 @@ def expositions_des_applications(applications: dict, domaines: dict, edge: str |
|
|||
return resultat
|
||||
|
||||
|
||||
def zones_inverses(adresses, supernet: str = "") -> list[str]:
|
||||
"""Les zones `in-addr.arpa` que cet ecosysteme revendique — et rien de plus.
|
||||
|
||||
Un TENANT tire un supernet /16 de son index : une seule zone suffit a couvrir tous ses
|
||||
sous-reseaux, et c'est celle qu'on rend. Un SITE ne derive pas — il declare plusieurs
|
||||
/24 et n'a pas de supernet unique. On rend alors une zone PAR /24 reellement occupe.
|
||||
|
||||
Revendiquer `0.10.in-addr.arpa` pour le site aurait ete plus simple et faux : cette
|
||||
zone couvre aussi la frontiere, le transit et les hyperviseurs, qui ne sont pas a lui.
|
||||
Une autorite qu'on s'attribue sans l'exercer est une panne differee — le resolveur
|
||||
repondrait NXDOMAIN pour des adresses qu'un autre sait nommer.
|
||||
|
||||
DEUX APPELANTS, UNE SEULE DERIVATION : `serveur_powerdns` ecrit ces zones,
|
||||
`serveur_resolveur` les delegue a l'autoritatif. Deux calculs separes finiraient par
|
||||
diverger, et la divergence ne se verrait qu'au premier PTR interroge.
|
||||
"""
|
||||
texte = str(supernet or "")
|
||||
if texte.endswith("/16") and texte.count(".") == 3:
|
||||
a, b = texte.split(".")[0], texte.split(".")[1]
|
||||
return [f"{b}.{a}.in-addr.arpa"]
|
||||
vues: list[str] = []
|
||||
for adresse in (adresses or []):
|
||||
bouts = str(adresse or "").split(".")
|
||||
if len(bouts) != 4 or not all(x.isdigit() for x in bouts):
|
||||
continue
|
||||
zone = f"{bouts[2]}.{bouts[1]}.{bouts[0]}.in-addr.arpa"
|
||||
if zone not in vues:
|
||||
vues.append(zone)
|
||||
return sorted(vues)
|
||||
|
||||
|
||||
def chaine_connexion(entree: dict, serveur: dict, secret: str = "***") -> str:
|
||||
"""Derive une chaine de connexion (mot de passe masque par defaut)."""
|
||||
type_bd = (serveur or {}).get("type", "postgres")
|
||||
|
|
|
|||
|
|
@ -1197,6 +1197,53 @@ def preuve_un_seul_maitre_du_plancher() -> tuple[bool, str]:
|
|||
f"et le gabarit maitre est pose ({gabarit[0]}).")
|
||||
|
||||
|
||||
def preuve_zones_inverses_couvrent_l_occupe() -> tuple[bool, str]:
|
||||
"""Les zones `in-addr.arpa` couvrent ce que l'ecosysteme occupe, et rien de plus.
|
||||
|
||||
POURQUOI (2026-08-25). La derivation supposait un supernet /16 — la forme d'un TENANT,
|
||||
qui tire tout de son index. Un SITE ne derive pas : il declare plusieurs /24 et n'a pas
|
||||
de supernet unique. La derivation rendait donc une chaine vide, aucune zone inverse
|
||||
n'etait generee, et les cinq machines du site restaient anonymes a l'envers — ce que le
|
||||
courriel et la journalisation reprochent en silence.
|
||||
|
||||
LE PIEGE INVERSE COMPTE AUTANT. Revendiquer `0.10.in-addr.arpa` aurait couvert le site
|
||||
d'un seul geste — et aussi la frontiere, le transit et les hyperviseurs, qui ne sont
|
||||
pas a lui. Une autorite qu'on s'attribue sans l'exercer est une panne differee : le
|
||||
resolveur repondrait NXDOMAIN pour des adresses qu'un autre sait nommer.
|
||||
|
||||
Cette preuve evalue la derivation elle-meme, sur les deux formes d'ecosysteme plus les
|
||||
cas degenerees. Le cas tenant DOIT rendre une seule zone /16, et le cas site DOIT en
|
||||
rendre quatre : sans ces deux-la, une derivation qui rend toujours une liste vide
|
||||
passerait la preuve sans rien garantir.
|
||||
"""
|
||||
from inventory_rules import zones_inverses as _zi
|
||||
|
||||
site = ["10.0.31.11", "10.0.32.11", "10.0.33.11", "10.0.33.21", "10.0.34.11"]
|
||||
cas = [
|
||||
("site — quatre reseaux declares, un /24 chacun", site, "10.0.34.0/24",
|
||||
["31.0.10.in-addr.arpa", "32.0.10.in-addr.arpa",
|
||||
"33.0.10.in-addr.arpa", "34.0.10.in-addr.arpa"]),
|
||||
("tenant — un supernet /16 couvre tout", ["10.27.15.11", "10.27.16.11"],
|
||||
"10.27.0.0/16", ["27.10.in-addr.arpa"]),
|
||||
("deux machines d'un meme /24 ne font qu'une zone",
|
||||
["10.0.33.11", "10.0.33.21"], "", ["33.0.10.in-addr.arpa"]),
|
||||
("aucune adresse : aucune zone, plutot qu'une zone fausse", [], "", []),
|
||||
("une adresse illisible est ignoree, pas devinee",
|
||||
["pas-une-adresse", "10.0.31.11"], "", ["31.0.10.in-addr.arpa"]),
|
||||
]
|
||||
manques: list[str] = []
|
||||
for titre, adresses, supernet, attendu in cas:
|
||||
obtenu = _zi(adresses, supernet)
|
||||
if obtenu != attendu:
|
||||
manques.append(f"{titre} : rend {obtenu}, attendu {attendu}")
|
||||
if manques:
|
||||
return False, ("Les zones inverses ne couvrent pas ce que l'ecosysteme occupe :"
|
||||
"\n - " + "\n - ".join(manques))
|
||||
return True, (f"Les zones inverses couvrent l'occupe et rien de plus "
|
||||
f"({len(cas)} cas evalues, dont un site a quatre zones et un tenant "
|
||||
f"a une).")
|
||||
|
||||
|
||||
|
||||
def preuve_glossaire_enseigne() -> tuple[bool, str]:
|
||||
"""Tout mot que le depot emploie devant l'exploitant est explique au glossaire.
|
||||
|
|
@ -1542,6 +1589,8 @@ PREUVES: list[dict] = [
|
|||
"refs": [], "func": preuve_parefeu_proxmox_sdn_seulement},
|
||||
{"id": "P46", "titre": "Plancher /etc/hosts : un seul role en decide",
|
||||
"refs": [], "func": preuve_un_seul_maitre_du_plancher},
|
||||
{"id": "P47", "titre": "Zones inverses : couvrir l'occupe, et rien de plus",
|
||||
"refs": [], "func": preuve_zones_inverses_couvrent_l_occupe},
|
||||
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
|
||||
"func": preuve_devis_frontiere_du_site},
|
||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue