2026-08-08 07:20:56 -04:00
|
|
|
# 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
|
2026-08-08 07:33:49 -04:00
|
|
|
make expositions-plan # chaque `expose:` du plan répond-il, depuis l'edge et depuis le poste
|
devis PostgreSQL et courriel : la serie des devis de service est complete
PostgreSQL — rend visibles deux defauts deja vecus ici : un reseau ECRIT dans
pg_hba au lieu d'etre derive, et une ligne host en clair la ou il faut hostssl
(verrou qui saute sans bruit, les clients verify-full continuant de marcher).
Interroge pg_settings, jamais le fichier : un grep de postgresql.conf annonce
ssl_cert_file=snakeoil alors que le serveur sert le certificat de l'AC — la
valeur vient d'un conf.d que le grep ne voyait pas. L'instrument etait
incomplet, pas la configuration.
Courriel — chaque maillon interroge la ou il dit la verite : postmap -q pour
la resolution LDAP de Postfix, doveadm user pour celle de Dovecot (le maillon
exact ou la livraison avait bloque), vraies conversations SMTP/IMAP. Verifie
aussi qu'une adresse INEXISTANTE ne resout pas — sinon la boite est un
fourre-tout et le devis ne mesure plus rien.
Trouve immediatement une divergence reelle : Dovecot connait la boite de
sysadmin@chezlepro.internal, Postfix ne sait pas y router (query_filter
(mail=%s), et l'attribut mail porte sysadmin@chezlepro.ca). Collision de
roles : une identite ne porte qu'une adresse mail et on lui en demande deux —
notification joignable hors du systeme, et cle de routage local. Arbitrage a
rendre avant correction.
Tests negatifs : PostgreSQL 3 ecarts nommes, code 1. Une variable morte
trouvee dans une branche que le cas nominal n'emprunte jamais.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 08:53:49 -04:00
|
|
|
make postgresql-plan # chiffrement imposé, et à quels réseaux
|
|
|
|
|
make courriel-plan # Postfix → LDAP → LMTP → Dovecot → IMAP
|
2026-08-08 07:20:56 -04:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
2026-08-08 07:33:49 -04:00
|
|
|
## 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.
|
|
|
|
|
|
devis PostgreSQL et courriel : la serie des devis de service est complete
PostgreSQL — rend visibles deux defauts deja vecus ici : un reseau ECRIT dans
pg_hba au lieu d'etre derive, et une ligne host en clair la ou il faut hostssl
(verrou qui saute sans bruit, les clients verify-full continuant de marcher).
Interroge pg_settings, jamais le fichier : un grep de postgresql.conf annonce
ssl_cert_file=snakeoil alors que le serveur sert le certificat de l'AC — la
valeur vient d'un conf.d que le grep ne voyait pas. L'instrument etait
incomplet, pas la configuration.
Courriel — chaque maillon interroge la ou il dit la verite : postmap -q pour
la resolution LDAP de Postfix, doveadm user pour celle de Dovecot (le maillon
exact ou la livraison avait bloque), vraies conversations SMTP/IMAP. Verifie
aussi qu'une adresse INEXISTANTE ne resout pas — sinon la boite est un
fourre-tout et le devis ne mesure plus rien.
Trouve immediatement une divergence reelle : Dovecot connait la boite de
sysadmin@chezlepro.internal, Postfix ne sait pas y router (query_filter
(mail=%s), et l'attribut mail porte sysadmin@chezlepro.ca). Collision de
roles : une identite ne porte qu'une adresse mail et on lui en demande deux —
notification joignable hors du systeme, et cle de routage local. Arbitrage a
rendre avant correction.
Tests negatifs : PostgreSQL 3 ecarts nommes, code 1. Une variable morte
trouvee dans une branche que le cas nominal n'emprunte jamais.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 08:53:49 -04:00
|
|
|
## 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.
|
|
|
|
|
|
2026-08-08 09:42:45 -04:00
|
|
|
Il vérifie aussi qu'une adresse **inexistante** ne résout pas. Ce n'est pas un luxe :
|
|
|
|
|
Dovecot a `allow_all_users = yes` et accepte donc n'importe quelle partie locale — le
|
|
|
|
|
garde-fou est entièrement chez Postfix.
|
|
|
|
|
|
|
|
|
|
**Et il regarde la file d'attente.** C'est la leçon la plus chère de la série : sa première
|
|
|
|
|
version disait `CONFORME` pendant que *toute* livraison interne était différée. Résolution
|
|
|
|
|
correcte, dialectes corrects, et rien qui arrive — Postfix exigeait TLS
|
|
|
|
|
(`lmtp_tls_security_level = verify`) vers un port Dovecot qui écoutait en clair. Les deux
|
|
|
|
|
côtés d'un même flux avaient été durcis séparément. **Un devis qui ne regarde que les
|
|
|
|
|
réglages ne dit pas si le service rend son service** ; la file est le seul endroit qui
|
|
|
|
|
révèle que le courrier ne bouge pas.
|
|
|
|
|
|
|
|
|
|
Le routage local se fait par **identifiant** (`uid`), pas par l'attribut `mail` — voir le
|
|
|
|
|
gabarit `ldap-mailboxes.cf.j2` pour la raison et le coût assumé.
|
devis PostgreSQL et courriel : la serie des devis de service est complete
PostgreSQL — rend visibles deux defauts deja vecus ici : un reseau ECRIT dans
pg_hba au lieu d'etre derive, et une ligne host en clair la ou il faut hostssl
(verrou qui saute sans bruit, les clients verify-full continuant de marcher).
Interroge pg_settings, jamais le fichier : un grep de postgresql.conf annonce
ssl_cert_file=snakeoil alors que le serveur sert le certificat de l'AC — la
valeur vient d'un conf.d que le grep ne voyait pas. L'instrument etait
incomplet, pas la configuration.
Courriel — chaque maillon interroge la ou il dit la verite : postmap -q pour
la resolution LDAP de Postfix, doveadm user pour celle de Dovecot (le maillon
exact ou la livraison avait bloque), vraies conversations SMTP/IMAP. Verifie
aussi qu'une adresse INEXISTANTE ne resout pas — sinon la boite est un
fourre-tout et le devis ne mesure plus rien.
Trouve immediatement une divergence reelle : Dovecot connait la boite de
sysadmin@chezlepro.internal, Postfix ne sait pas y router (query_filter
(mail=%s), et l'attribut mail porte sysadmin@chezlepro.ca). Collision de
roles : une identite ne porte qu'une adresse mail et on lui en demande deux —
notification joignable hors du systeme, et cle de routage local. Arbitrage a
rendre avant correction.
Tests negatifs : PostgreSQL 3 ecarts nommes, code 1. Une variable morte
trouvee dans une branche que le cas nominal n'emprunte jamais.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 08:53:49 -04:00
|
|
|
|
doctrine : D-68 et D-69 — relire ce qu'on ecrit, pas « toujours l'API »
Question de l'exploitant apres deux pannes causees par kcadm. La reponse est
non, et elle se fonde sur la mesure : sur six familles de defauts du jour,
deux seulement viennent d'un CLI ; un module Ansible (ldap_entry) a commis la
meme faute, et trois autres viennent d'un grep de fichier, de la precedence
Ansible et de mon propre comparateur.
D-68 — ecrire, puis relire et comparer, quelle que soit l'interface ; choisir
celle dont le chemin de lecture parle le meme langage que celui d'ecriture.
Une API est souvent preferable parce qu'elle rend la ressource ENTIERE, ce qui
permet le patron de chaque devis. Mais la plupart de la flotte n'a pas d'API,
et postconf -h / -e sont parfaitement symetriques.
D-69 — sur Keycloak : l'API pour toute map ou collection, kcadm ailleurs
(vocabulaire de la doc du produit, donc lisible sans IA).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 11:45:31 -04:00
|
|
|
## La règle dont ces devis découlent (D-68)
|
|
|
|
|
|
|
|
|
|
> **Écrire, puis relire et comparer** — quelle que soit l'interface. Choisir celle dont le
|
|
|
|
|
> chemin de *lecture* parle le même langage que le chemin d'*écriture*.
|
|
|
|
|
|
|
|
|
|
Ce n'est pas « toujours préférer l'API ». Cette règle-là n'aurait prédit aucune des pannes
|
|
|
|
|
du 2026-08-08. Sur six familles de défauts ce jour-là, **deux** venaient d'un CLI :
|
|
|
|
|
|
|
|
|
|
| Ce qui a menti | Interface |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `kcadm -s` sur une map : succès, rien d'écrit | CLI |
|
|
|
|
|
| `grafana-cli` : « password changed ✔ » dans une base fantôme | CLI |
|
|
|
|
|
| `ldap_entry` : crée, ne modifie jamais | module Ansible |
|
|
|
|
|
| `grep postgresql.conf` : valeur fausse (surcharge `conf.d`) | lecture de fichier |
|
|
|
|
|
| `include_vars` : le devis rapportait le défaut, pas le réel | précédence Ansible |
|
|
|
|
|
| le comparateur d'expositions : un 502 compté comme vivant | mon propre code |
|
|
|
|
|
|
|
|
|
|
Le facteur commun n'est pas l'interface, c'est d'avoir **écrit sans relire**. Et la plupart
|
|
|
|
|
de la flotte n'a pas d'API du tout — Postfix, Dovecot, nginx, slapd, nftables. `postconf -h`
|
|
|
|
|
et `postconf -e` sont parfaitement symétriques : lecture et écriture dans le même
|
|
|
|
|
vocabulaire, c'est tout ce qu'on demande.
|
|
|
|
|
|
|
|
|
|
Une API est souvent préférable, mais pour une raison précise : elle rend la ressource
|
|
|
|
|
**entière**, ce qui permet le patron qu'on retrouve dans chaque devis — *fusionner
|
|
|
|
|
l'attendu dans le réel ; si rien ne change, c'est conforme*. Une interface qui n'accepte
|
|
|
|
|
que des écritures ne peut pas le soutenir.
|
|
|
|
|
|
|
|
|
|
**Les cinq devis sont cette relecture**, faite après coup et par une autre main que celle
|
|
|
|
|
qui a écrit. C'est ce qui les distingue d'un déploiement : `make deployer` réconcilie, les
|
|
|
|
|
devis constatent.
|
|
|
|
|
|
2026-08-08 07:20:56 -04:00
|
|
|
## 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.
|