dns : quatre zones inverses pour le site, et rien de plus
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:
Daniel Allaire 2026-08-25 20:58:50 -04:00
parent a74e35bf21
commit e5841ea684
12 changed files with 284 additions and 31 deletions

View file

@ -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

View file

@ -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

View file

@ -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,
}

View file

@ -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('')) }}

View file

@ -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 }}"

View file

@ -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

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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é.

View file

@ -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:

View file

@ -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")

View file

@ -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": [],