# Les devis de service : ce qui tourne correspond-il à ce qui est déclaré ? > **Instrument ajouté le 2026-08-08**, après une série de défauts qu'aucun test n'avait > signalés. Lecture seule — il ne modifie rien. ``` make identite-plan # realm, fédération, mappeurs, politique, comptes make certificats-plan # certificats sur disque contre certificats réellement servis make expositions-plan # chaque `expose:` du plan répond-il, depuis l'edge et depuis le poste make postgresql-plan # chiffrement imposé, et à quels réseaux make courriel-plan # Postfix → LDAP → LMTP → Dovecot → IMAP ``` ## Le trou qu'il comble `scripts/prouver.py` porte 30 preuves. Elles sont toutes **statiques** : elles lisent le dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est cohérent **avec lui-même** — que les handlers existent, que les intrants ont un propriétaire, que rien n'est codé en dur. Aucune ne demande au système déployé s'il ressemble à ce que le dépôt annonce. Les quatre défauts du 2026-08-08 vivaient tous dans cet angle : | Défaut | Le dépôt disait | La machine portait | |---|---|---| | politique de mot de passe | `length(12)…` | *aucune* — `abcd` accepté | | entrée `ppolicy` figée | `pwdMustChange: TRUE` | `FALSE` | | fédération en écriture | comptes écrits dans LDAP | `syncRegistrations` absent | | prénom | `givenName` | `cn` (le nom complet) | Chacun a été trouvé en relisant après avoir écrit. Aucun n'a été signalé. ## Ce qu'il vérifie Le playbook `playbooks/maintenance/devis-identite.yml` **relève** deux dictionnaires — le déclaré et le réel — et les dépose en JSON. `scripts/devis_identite.py` **compare**. La séparation est délibérée : la comparaison en Jinja est illisible, et le dépôt a déjà cette forme pour les devis réseau (Python raisonne, Ansible relève). - **realm** — politique de mot de passe, anti-force-brute, « mot de passe oublié », relais SMTP - **fédération** — mode d'écriture et les six clés de durcissement - **mappeurs** — chaque attribut LDAP projeté - **annuaire** — l'entrée `pwdPolicy`, attribut par attribut - **comptes** — ceux qui n'ont pas d'adresse, pour qui « mot de passe oublié » ne mène nulle part Le déclaré n'est jamais recopié ici : le playbook charge les défauts du rôle et appelle les résolveurs. **Un devis qui redéclare ce qu'il vérifie ne vérifie rien.** ## Le devis des certificats Il traque un défaut connu et récurrent : un certificat renouvelé **sur disque** mais toujours servi **périmé** depuis la mémoire du service, tant que nginx / postfix / dovecot / slapd / step-ca n'ont pas été rechargés. **Ce qu'il a fallu désapprendre pour l'écrire.** Les certificats step-ca vivent 24 h et le minuteur les renouvelle toutes les ~14 minutes. Une empreinte servie *différente* de celle sur disque est donc l'état **normal** : comparer les empreintes aurait produit un vérificateur qui crie en permanence, et qu'on aurait appris à ignorer. Le signal utile est l'**échéance de ce qui est réellement servi**, plus l'absence de `client_pki_reload_services` — car alors rien ne rafraîchira jamais la copie en mémoire. Il a trouvé quelque chose à sa première exécution : sur `infra-pki-01`, l'autorité elle-même, le certificat était **expiré depuis plus de huit heures** et le renouvellement échouait toutes les quatorze minutes sur `'step ca renew' requires the '--ca-url' flag`. Rien ne le signalait. ## Le devis des expositions Il pose la question que se pose vraiment un utilisateur : **est-ce que mes services répondent ?** Et quand la réponse est non, il dit *où* ça casse. **Une vraie requête, jamais un `connect()`.** À travers l'OPNsense (anti-spoofing), toute connexion TCP réussit — y compris vers une adresse où aucune machine n'existe. Et « lire des données après connexion » ne vaut rien en TLS, où c'est le *client* qui parle en premier : le port 443 d'un edge sain se comporte exactement comme un port mort. Seul le code de retour d'une requête HTTPS complète tranche. **Deux points de vue, et c'est leur différence qui diagnostique :** | Point de vue | Ce qu'il traverse | |---|---| | depuis l'edge | edge + dorsal | | depuis le poste | DNS + frontière + edge + dorsal — le chemin d'un vrai utilisateur | L'un répond et pas l'autre : ce n'est pas le service, c'est le chemin. **Un code n'est pas un verdict.** Un 502 ou un 504 *est* une réponse, et pourtant le dorsal est mort derrière. Le premier jet ne testait que la présence d'un code et laissait donc passer un dorsal tombé — trouvé par le test négatif, pas par la relecture. Un 302 ou un 401, en revanche, est un service parfaitement vivant qui redirige vers l'IdP ou exige une authentification. ## Le devis PostgreSQL Deux défauts déjà vécus dans ce dépôt, et qu'il rend visibles : un réseau **écrit** dans `pg_hba.conf` au lieu d'être dérivé (`10.11.0.0/16` sous un commentaire « AJUSTER » que personne n'a suivi), et une ligne `host` en clair là où il faut `hostssl` — le verrou saute sans bruit, puisque les clients en `verify-full` continuent de fonctionner. **Interroger l'état effectif, jamais le fichier.** Un `grep` de `postgresql.conf` annonce `ssl_cert_file = snakeoil` sur `data-sql-01`, alors que le serveur sert bien le certificat de l'AC : la valeur vient d'un `conf.d/99-setops.conf` que le grep ne voyait pas. C'était l'instrument qui était incomplet, pas la configuration. Le devis lit `pg_settings`. ## Le devis du courriel La chaîne est longue — Postfix → LDAP → LMTP → Dovecot → IMAP — et chaque maillon peut casser sans que le suivant s'en aperçoive. Chacun est donc interrogé là où il dit la vérité : `postmap -q` pour la résolution LDAP de Postfix, `doveadm user` pour celle de Dovecot — précisément le maillon où la livraison avait bloqué — et une vraie conversation SMTP/IMAP pour ce que les serveurs annoncent. Il vérifie aussi qu'une adresse **inexistante** ne résout pas : sans ce contrôle, une boîte fourre-tout accepterait n'importe quel nom, et le devis ne mesurerait plus rien. ## Un piège de construction, à connaître avant d'écrire le prochain `include_vars` au niveau du *play* **prime sur les `group_vars`**. Charger les défauts d'un rôle tels quels fait donc mentir le devis : il compare au défaut du dépôt au lieu de la valeur réellement déclarée par l'inventaire. Le premier jet rapportait `client_pki_reload_services: []` sur les quatorze hôtes alors que quatre groupes le déclarent — **le devis portait exactement le défaut qu'il est censé traquer.** La forme correcte, reprise dans les deux devis : 1. retenir ce que l'inventaire déclare, **avant** tout chargement de défauts ; 2. charger les défauts du rôle (`include_vars`, sans `name:` — les défauts se citent entre eux, et ces références ne se résolvent plus une fois enfermées dans un dictionnaire) ; 3. réimposer les valeurs de l'inventaire **clé par clé**, en boucle. `set_fact` n'accepte pas un dictionnaire entier passé en argument libre : il le prend sans erreur et n'en fait rien — c'est ainsi que le premier correctif a paru fonctionner tout en laissant le devis mentir. ## Ce qu'ils ne font pas Ils ne corrigent pas — c'est `make deployer` qui réconcilie. Ils répondent à l'autre question, et sortent en code 1 s'il y a un écart. Ils couvrent l'identité et les certificats. Le courriel, la base de données et les expositions web attendent le même traitement ; le patron est là pour être repris.