TLS des bases : le consommateur suit son serveur, il ne le devine plus
La supervision du SITE etait morte depuis 11:10 et rien ne le disait. icingadb refuse par pg_hba — hostssl impose cote serveur, connexion en clair cote client. icinga2 tournait, redis tournait, les sondes poussaient, et rien n atteignait la base : les verdicts se calculaient dans le vide. La cause est une seconde liste tenue a la main. tls_force allume hostssl ; chaque consommateur avait SON interrupteur a allumer dans les group_vars. Chezlepro avait les trois, le site avait le premier. serveur_forgejo disait pire que rien : sslmode disable ecrit en dur, le contraire de ce que le serveur imposait. resoudre_base expose resoudre_base_db_tls_force, lu dans les hostvars de la machine qui PORTE la base. Les trois interrupteurs en derivent. Un serveur qui ne declare rien ne force rien : on ne casse pas un ecosysteme qui n a pas bascule. P78 refuse une valeur ecrite chez un consommateur, et nomme les deux roles sans reglage TLS plutot que de rendre un vert muet sur eux. Apres : icingadb active, TLSv1.3 vu par PostgreSQL, 16 hotes et 88 services en base. Les cinq sondes des marqueurs du site, INCONNU faute de deploiement, rapportent leur phrase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
d4bc3e48b7
commit
c308c9c5b0
9 changed files with 206 additions and 7 deletions
|
|
@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
|||
## Écrire, puis relire (D-68)
|
||||
|
||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||
C'est aussi ce que font les 77 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
C'est aussi ce que font les 78 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||
dépôt annonce.**
|
||||
|
||||
|
|
|
|||
81
CHANGELOG.md
81
CHANGELOG.md
|
|
@ -1,5 +1,86 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-14 (18) — La supervision du SITE etait morte depuis 11:10, et rien ne le disait
|
||||
|
||||
L'exploitant : *« je ne vois pas de serveur icinga pour le site ? »*. Il y en a un. Il
|
||||
tournait. Et il ne servait a rien.
|
||||
|
||||
### La panne
|
||||
|
||||
icingadb.service : failed depuis 11:10:27
|
||||
|
||||
pq: aucune entree dans pg_hba.conf pour l'hote « 10.37.36.11 »,
|
||||
utilisateur « icingadb », base « icingadb », aucun chiffrement
|
||||
|
||||
`icinga2` tournait, `icingadb-redis` tournait, les sondes poussaient — et **rien
|
||||
n'atteignait la base**. Les verdicts se calculaient dans le vide. Aucun deploiement n'a
|
||||
echoue, aucun service visible n'est tombe : le moteur de supervision etait mort et
|
||||
personne n'avait de quoi l'apprendre, puisque c'est precisement lui qui le dirait.
|
||||
|
||||
### La cause : une seconde liste, tenue a la main
|
||||
|
||||
`serveur_postgresql_tls_force` pose `hostssl` dans `pg_hba` — toute connexion non chiffree
|
||||
refusee. Chaque consommateur avait alors SON interrupteur a allumer separement, dans les
|
||||
`group_vars` de chaque ecosysteme :
|
||||
|
||||
| | Chezlepro | SITE |
|
||||
|---|---|---|
|
||||
| `serveur_postgresql_tls_force` | true | **true** |
|
||||
| `serveur_icinga_db_tls` | true | *absent* |
|
||||
| `serveur_keycloak_db_sslmode` | verify-full | *absent* |
|
||||
|
||||
Chezlepro avait les trois. Le site avait le premier. Et le commentaire de
|
||||
`site_inventaire.py` cite deja cette erreur mot pour mot : le cote SERVEUR avait ete
|
||||
corrige le 2026-09-12 (`reseaux_autorises`), le cote CONSOMMATEUR jamais.
|
||||
|
||||
`serveur_forgejo` disait pire que rien : `sslmode: "disable"` ecrit en dur. Le
|
||||
consommateur affirmait le contraire de ce que le serveur imposait.
|
||||
|
||||
### Le remede : le consommateur suit son serveur
|
||||
|
||||
`resoudre_base` expose desormais `resoudre_base_db_tls_force`, lu dans les `hostvars` de
|
||||
la machine qui PORTE la base. Un consommateur n'a plus a savoir qu'il doit chiffrer — il
|
||||
le deduit de ce que sert son serveur. Les trois interrupteurs en derivent.
|
||||
|
||||
`default(false)` : un serveur qui ne declare rien ne force rien, et le consommateur reste
|
||||
en clair. On ne casse pas un ecosysteme qui n'a pas bascule.
|
||||
|
||||
**P78** refuse qu'un consommateur porte une valeur ECRITE : elle doit deriver. Une valeur
|
||||
ecrite est une seconde liste, et une liste qui suit une autre prend du retard. La preuve
|
||||
nomme aussi les deux consommateurs SANS reglage TLS — `serveur_icingaweb2`,
|
||||
`serveur_nextcloud` — plutot que de rendre un vert muet sur eux.
|
||||
|
||||
### Apres
|
||||
|
||||
icingadb active
|
||||
connexions vues par PostgreSQL icingadb | t | TLSv1.3 | 10.37.36.11
|
||||
en base 16 hotes, 88 services
|
||||
|
||||
Et les cinq sondes ecrites hier pour les marqueurs du site etaient **INCONNU** : Icinga
|
||||
avait bien defini les services depuis leurs `meta/supervision.yml`, mais leurs roles
|
||||
n'avaient pas ete redeployes, donc aucun script n'etait pose. Les cinq roles appliques,
|
||||
un rapport force, et elles disent leur phrase :
|
||||
|
||||
site-backup-01 depot-locataires OK 2 locataire(s) etanche(s), 374 Go libres
|
||||
site-cache-01 cache-site-racine OK racine sans amont, et l'index se sert
|
||||
site-dns-01 resolution-locataires OK 3 supernet(s) de locataire admis
|
||||
site-forge-01 genome-servi OK 6 depots servis sous « genome », aucun vide
|
||||
site-ops-01 pouvoir-materialiser OK carte en place, voute chiffree, cle en 600
|
||||
|
||||
### Ce que la supervision, une fois vivante, a immediatement signale
|
||||
|
||||
site-mon-01 journaux-frontiere CRITIQUE LA FRONTIERE NE JOURNALISE PLUS
|
||||
site-forge-01 / site-pki-01 / site-mon-01 sante CRITIQUE setops-verification-depot.service
|
||||
|
||||
Deux constats a instruire, et c'est exactement ce qu'on attend d'un moteur qu'on vient de
|
||||
remettre en marche : il ne rassure pas, il rapporte.
|
||||
|
||||
### Ce qui manque encore, et c'est une decision
|
||||
|
||||
Le plan du site declare `serveur_icinga` et **pas** `serveur_icingaweb2`. Le site calcule
|
||||
88 verdicts que personne ne peut LIRE. Grafana montre les series, pas les verdicts. Ajouter
|
||||
la console est une machine de zero (elle se co-localise) et un nom expose de plus.
|
||||
|
||||
## 2026-09-14 (17) — La garde regardait un seul cote de la cloture
|
||||
|
||||
L'exploitant, en cherchant son tableau : *« grafana c'etait observatoire.genese.internal »*.
|
||||
|
|
|
|||
|
|
@ -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/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (76 OK · 0 echec · 1 saute)
|
||||
- **Verdict** : ✅ CONFORME (77 OK · 0 echec · 1 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (77 preuves, 68 roles, 41 groupes). |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (78 preuves, 68 roles, 41 groupes). |
|
||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `392da4d` (publie le 2026-09-14). |
|
||||
|
|
@ -90,6 +90,7 @@
|
|||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 152 gabarits de role : tous se rendent. |
|
||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
||||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
|||
|
||||
## Le trou qu'il comble
|
||||
|
||||
`scripts/prouver.py` porte 77 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
`scripts/prouver.py` porte 78 preuves (dont une conditionnelle, sautée sans la clé de la voûte). 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.
|
||||
|
|
|
|||
|
|
@ -64,3 +64,33 @@
|
|||
# plusieurs tenants) + nom canonique pour la vérif TLS. Résolu par le plancher /etc/hosts.
|
||||
resoudre_base_db_host: "{{ (serveurs_bd | default({}))[resoudre_base_entree.serveur].hote }}.{{ domaine_interne }}"
|
||||
resoudre_base_db_port: "{{ (serveurs_bd | default({}))[resoudre_base_entree.serveur].port | default(5432) }}"
|
||||
|
||||
# LA POSTURE TLS DU SERVEUR, LUE CHEZ LE SERVEUR (2026-09-14).
|
||||
#
|
||||
# CE QUE ÇA A COÛTÉ. `serveur_postgresql_tls_force` pose `hostssl` dans `pg_hba` : toute
|
||||
# connexion non chiffrée est refusée. Chaque consommateur avait alors SON propre
|
||||
# interrupteur — `serveur_icinga_db_tls`, `serveur_keycloak_db_sslmode`,
|
||||
# `serveur_forgejo_db_sslmode` — qu'il fallait allumer à la main, dans les `group_vars`,
|
||||
# en même temps que celui du serveur.
|
||||
#
|
||||
# Chezlepro les avait tous les trois. Le site n'avait que celui du serveur. `icingadb` y
|
||||
# est donc tombé le 2026-09-14 à 11:10 et n'est jamais remonté :
|
||||
#
|
||||
# aucune entrée dans pg_hba.conf pour l'hôte « 10.37.36.11 »,
|
||||
# utilisateur « icingadb », base « icingadb », aucun chiffrement
|
||||
#
|
||||
# Une liste qui suit une autre prend du retard. Ici la seconde liste était tenue à la
|
||||
# main dans un dépôt d'écosystème, et rien ne la comparait à la première.
|
||||
#
|
||||
# ON LIT DONC LA POSTURE CHEZ CELUI QUI LA DÉCIDE. Le serveur de base est un hôte de
|
||||
# l'inventaire ; ce qu'il applique est dans ses `hostvars`. Un consommateur n'a plus à
|
||||
# savoir qu'il doit chiffrer : il le déduit de ce que sert son serveur.
|
||||
#
|
||||
# `default(false)` : un serveur qui ne déclare rien ne force rien, et le consommateur
|
||||
# reste en clair — c'était l'état de toute la flotte avant le zéro-confiance, et on ne
|
||||
# casse pas un écosystème qui n'a pas encore basculé.
|
||||
- name: Le serveur de base exige-t-il des connexions chiffrées ?
|
||||
ansible.builtin.set_fact:
|
||||
resoudre_base_db_tls_force: >-
|
||||
{{ (hostvars[(serveurs_bd | default({}))[resoudre_base_entree.serveur].hote]
|
||||
| default({})).get('serveur_postgresql_tls_force', false) | bool }}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,11 @@ serveur_forgejo_db_chemin: "{{ serveur_forgejo_data }}/forgejo.db"
|
|||
# TLS vers PostgreSQL (zero-confiance). 'disable' = compat ; 'verify-full' = chiffre + verifie
|
||||
# le cert serveur contre le root_ca step-ca (via PGSSLROOTCERT dans l'unite systemd).
|
||||
# Sans objet en `sqlite` : il n'y a pas de connexion reseau a chiffrer.
|
||||
serveur_forgejo_db_sslmode: "disable"
|
||||
# DÉRIVÉ DE CE QUE SERT LE SERVEUR, plus posé à la main. Voir `resoudre_base`.
|
||||
# `disable` restait écrit en dur ici alors que le serveur pouvait exiger TLS : le
|
||||
# consommateur affirmait le contraire de ce que le serveur imposait.
|
||||
serveur_forgejo_db_sslmode: >-
|
||||
{{ 'verify-full' if (resoudre_base_db_tls_force | default(false)) else 'disable' }}
|
||||
serveur_forgejo_db_sslrootcert: "/etc/step/certs/root_ca.crt"
|
||||
|
||||
# --- Habilitation par groupe (D-66) -------------------------------------------
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ serveur_icinga_schema: "/usr/share/icingadb/schema/pgsql/schema.sql"
|
|||
|
||||
# TLS vers PostgreSQL (zero-confiance). true = IcingaDB verifie le cert serveur
|
||||
# contre le root_ca step-ca (host dans le SAN). root_ca doit etre lisible (0644).
|
||||
serveur_icinga_db_tls: false
|
||||
# DÉRIVÉ DE CE QUE SERT LE SERVEUR, plus posé à la main. Voir `resoudre_base`.
|
||||
serveur_icinga_db_tls: "{{ resoudre_base_db_tls_force | default(false) }}"
|
||||
serveur_icinga_db_ca: "/etc/step/certs/root_ca.crt"
|
||||
|
||||
# --- Identite TLS de l'API (5665) ---
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ serveur_keycloak_role_assignments: []
|
|||
|
||||
# TLS vers PostgreSQL (zero-confiance). Vide = pas de verif (compat). 'verify-full' =
|
||||
# chiffre + verifie le cert serveur contre le root_ca step-ca (l'hote doit etre dans le SAN).
|
||||
serveur_keycloak_db_sslmode: ""
|
||||
# DÉRIVÉ DE CE QUE SERT LE SERVEUR, plus posé à la main. Voir `resoudre_base`.
|
||||
serveur_keycloak_db_sslmode: >-
|
||||
{{ 'verify-full' if (resoudre_base_db_tls_force | default(false)) else '' }}
|
||||
serveur_keycloak_db_sslrootcert: "/etc/step/certs/root_ca.crt"
|
||||
|
||||
# --- Projection des groupes LDAP (D-65) ---------------------------------------
|
||||
|
|
|
|||
|
|
@ -2179,6 +2179,84 @@ def preuve_panneaux_assembles() -> tuple[bool, str]:
|
|||
f"expression, raison et une unite que la table sait traduire.")
|
||||
|
||||
|
||||
def preuve_consommateurs_suivent_le_verrou_tls() -> tuple[bool, str]:
|
||||
"""Un consommateur de base ne decide pas seul s'il chiffre : il suit son serveur.
|
||||
|
||||
LE DEFAUT, MESURE LE 2026-09-14 A 11:10. `serveur_postgresql_tls_force` pose `hostssl`
|
||||
dans `pg_hba` : toute connexion NON chiffree est refusee. Chaque consommateur avait
|
||||
alors SON interrupteur — `serveur_icinga_db_tls`, `serveur_keycloak_db_sslmode`,
|
||||
`serveur_forgejo_db_sslmode` — a allumer A LA MAIN, dans les `group_vars`, en meme
|
||||
temps que celui du serveur.
|
||||
|
||||
Chezlepro les avait tous les trois. Le SITE n'avait que celui du serveur :
|
||||
|
||||
aucune entree dans pg_hba.conf pour l'hote « 10.37.36.11 »,
|
||||
utilisateur « icingadb », base « icingadb », aucun chiffrement
|
||||
|
||||
`icingadb` est tombe et n'est jamais remonte. Le moteur de supervision tournait,
|
||||
Redis tournait, et plus rien n'atteignait la base : les verdicts se calculaient dans
|
||||
le vide. Une panne qui ne fait echouer aucun deploiement.
|
||||
|
||||
ET `serveur_forgejo` DISAIT PIRE QUE RIEN : `sslmode: "disable"` en dur. Le
|
||||
consommateur affirmait le contraire de ce que le serveur imposait.
|
||||
|
||||
CE QUE CETTE PREUVE EXIGE : que l'interrupteur de chaque consommateur DERIVE de
|
||||
`resoudre_base_db_tls_force` — la posture lue chez le serveur — au lieu de porter une
|
||||
valeur ecrite. Une valeur ecrite est une seconde liste, et une liste qui suit une
|
||||
autre prend du retard.
|
||||
|
||||
CE QU'ELLE NE FAIT PAS. Elle ne verifie pas qu'une connexion CHIFFREE aboutit : ca
|
||||
demande un serveur, un certificat et une AC. C'est au deploiement de le dire, et il
|
||||
le dit — un consommateur qui n'y arrive pas ne demarre pas.
|
||||
"""
|
||||
roles = RACINE / "roles"
|
||||
consommateurs: list[str] = []
|
||||
for taches in sorted(roles.glob("*/tasks/main.yml")):
|
||||
role = taches.parent.parent.name
|
||||
if role == "resoudre_base":
|
||||
continue
|
||||
corps = taches.read_text(encoding="utf-8")
|
||||
meta = roles / role / "meta" / "main.yml"
|
||||
if meta.is_file():
|
||||
corps += meta.read_text(encoding="utf-8")
|
||||
if "resoudre_base" in corps:
|
||||
consommateurs.append(role)
|
||||
|
||||
if not consommateurs:
|
||||
return True, "Aucun role ne consomme de base : rien a verifier."
|
||||
|
||||
fautes, suivis, sans_reglage = [], [], []
|
||||
for role in consommateurs:
|
||||
f = roles / role / "defaults" / "main.yml"
|
||||
brut = f.read_text(encoding="utf-8") if f.is_file() else ""
|
||||
# Les lignes de reglage TLS, quelle que soit la forme du nom.
|
||||
lignes = [l for l in brut.splitlines()
|
||||
if re.match(r"\s*[a-z_]*_db_(tls|sslmode|ssl_mode)\s*:", l)]
|
||||
if not lignes:
|
||||
# PAS UNE FAUTE : un consommateur peut n'avoir aucun reglage parce que son
|
||||
# client chiffre par defaut, ou parce qu'il parle a SQLite. On le DIT, ce qui
|
||||
# vaut mieux qu'un vert muet — c'est la carte de ce qui reste a regarder.
|
||||
sans_reglage.append(role)
|
||||
continue
|
||||
# La valeur derivee peut etre sur la ligne ou sur la suivante (`>-`).
|
||||
bloc = brut
|
||||
if "resoudre_base_db_tls_force" in bloc:
|
||||
suivis.append(role)
|
||||
else:
|
||||
fautes.append(f"{role} : {lignes[0].strip()} — valeur ecrite, elle ne suit "
|
||||
f"pas `resoudre_base_db_tls_force`")
|
||||
|
||||
if fautes:
|
||||
return False, ("Un consommateur decide seul s'il chiffre, au lieu de suivre son "
|
||||
"serveur :\n - " + "\n - ".join(fautes)
|
||||
+ "\n Un `hostssl` allume cote serveur le fera tomber, sans "
|
||||
"qu'aucun deploiement n'echoue.")
|
||||
detail = f"{len(suivis)} consommateur(s) suivent la posture de leur serveur"
|
||||
if sans_reglage:
|
||||
detail += f" ; {len(sans_reglage)} sans reglage TLS ({', '.join(sans_reglage)})"
|
||||
return True, detail + "."
|
||||
|
||||
|
||||
def preuve_gabarits_shell_rendent() -> tuple[bool, str]:
|
||||
"""Aucun gabarit shell ne contient de sequence que Jinja lirait autrement.
|
||||
|
||||
|
|
@ -4015,6 +4093,8 @@ PREUVES: list[dict] = [
|
|||
"refs": [], "func": preuve_gabarits_shell_rendent},
|
||||
{"id": "P77", "titre": "Panneaux declares : assemblables, et gradues",
|
||||
"refs": [], "func": preuve_panneaux_assembles},
|
||||
{"id": "P78", "titre": "Un consommateur de base suit le verrou TLS de son serveur",
|
||||
"refs": [], "func": preuve_consommateurs_suivent_le_verrou_tls},
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue