From 7d13a8d76415506f3c69e7ece2a06a28906d16e2 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Tue, 28 Jul 2026 23:23:18 +0200 Subject: [PATCH] =?UTF-8?q?doc(pgbouncer)=20=E2=80=94=20la=20version=20mes?= =?UTF-8?q?ur=C3=A9e=20n'=C3=A9tait=20pas=20la=20d=C3=A9ploy=C3=A9e,=20et?= =?UTF-8?q?=20DISCARD=20ALL=20a=20un=20prix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois corrections au raisonnement, toutes issues d'une contre-vérification qui a refait les mesures au lieu de les relire. 1. La mesure invoquait pgbouncer 1.25.2 « la version en prod ». Le pod tourne 1.23.1 (ghcr.io/icoretech/pgbouncer-docker:1.23.1-fixed, chart 2.3.1, up 133 j). Tout a été rejoué sur 1.23.1 avec la ConfigMap extraite du cluster : les conclusions tiennent, mais la preuve invoquée portait sur autre chose. 2. La portée de la fuite était laissée en suspens, donc lue au pire. Les pools sont partitionnés par (base, utilisateur) — 210 pools, aucun mélange. Le seul héritier possible du SET ROLE de kadans-api est kadans-api. Défaut d'hygiène, pas brèche inter-applications. Le dire baisse la gravité, et c'est plus utile qu'une alarme vague. 3. ⚠ Le point manquant, et c'est l'inverse de ce qu'on croyait : DISCARD ALL rend la protection de db.go STRICTEMENT PLUS FRAGILE. Aujourd'hui (DEALLOCATE ALL) elle survivrait à un passage en transaction pooling ; après, non — le SET ROLE d'AfterConnect est effacé entre deux transactions, et poserRoleProprietaire ne peut pas le voir puisqu'il relit current_role à l'ouverture. Sans danger tant que pool_mode reste `session` (le défaut, non déclaré) — mais c'est précisément le genre de trappe qui se referme des mois plus tard sur quelqu'un qui optimise. Les quatre combinaisons sont mesurées et écrites au-dessus de la ligne. La ceinture Vault couvre ce cas (un défaut de rôle survit à DISCARD ALL), mais seulement au renouvellement des baux. D'où l'avertissement explicite. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GdUCA5Uz8QyMwa2P4Pg2hK --- pgbouncer/values.yaml | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/pgbouncer/values.yaml b/pgbouncer/values.yaml index 48295b6..314018f 100644 --- a/pgbouncer/values.yaml +++ b/pgbouncer/values.yaml @@ -22,10 +22,20 @@ pgbouncer: &pgbouncer_config # `DEALLOCATE ALL` ne nettoie QUE les requêtes préparées — d'où sa mise en # place (07e2c6d, « prepared statement already exists » de crowdsec). # Tout le reste de l'état de session passait au suivant. MESURÉ contre un - # pgbouncer 1.25.2 monté avec CETTE configuration (session, pool_size=1) : - # le client 2, qui n'avait rien demandé, héritait de `work_mem=17MB`, du - # `LISTEN canal_test` du client 1, de sa table TEMP — et de son `SET ROLE`, - # au point de créer des tables appartenant à un autre rôle que le sien. + # pgbouncer **1.23.1** — la version RÉELLEMENT déployée (image + # ghcr.io/icoretech/pgbouncer-docker:1.23.1-fixed, chart pgbouncer-2.3.1), + # montée avec CETTE configuration extraite du cluster (session, + # pool_size=1) : le client 2, qui n'avait rien demandé, héritait de + # `work_mem=17MB`, du `LISTEN canal_test` du client 1, de sa table TEMP — + # et de son `SET ROLE`, au point de créer des tables appartenant à un + # autre rôle que le sien. + # + # Portée de la fuite, mesurée et non supposée : les pools sont partitionnés + # par (base, utilisateur) — 210 pools, aucun ne mélange deux bases ni deux + # comptes. Le seul héritier possible d'un `SET ROLE` posé par kadans-api + # est un client de la base `kadans` avec le MÊME identifiant éphémère, + # c'est-à-dire kadans-api elle-même. Défaut d'hygiène, pas brèche + # inter-applications. # # `DISCARD ALL` est le défaut de pgbouncer, et c'est un SUR-ENSEMBLE strict # des deux valeurs qui l'ont précédé ici : il contient `DEALLOCATE ALL` @@ -37,6 +47,29 @@ pgbouncer: &pgbouncer_config # la connexion serveur n'est rendue qu'à la déconnexion du client, donc le # reset ne court jamais entre deux requêtes d'une même session. Vérifié : # table TEMP, `SET work_mem` et `LISTEN` d'un client VIVANT survivent. + # + # ⚠⚠ CE QUE CETTE LIGNE REND FRAGILE — et c'est l'inverse de ce qu'on croit. + # + # `pool_mode` n'est PAS déclaré ici, donc il vaut `session`, le défaut. + # C'est ce qui rend `DISCARD ALL` sans danger. **Le jour où quelqu'un + # écrira `pool_mode: transaction`, il cassera kadans-api en silence** : + # son `SET ROLE` est posé UNE FOIS à l'ouverture (`AfterConnect`, db.go), + # et en transaction pooling `DISCARD ALL` court ENTRE deux transactions — + # donc le rôle est effacé avant les migrations suivantes. MESURÉ, les + # quatre combinaisons, propriétaire de la table créée : + # + # session + DEALLOCATE ALL → rôle stable ✅ (mais la fuite reste) + # session + DISCARD ALL → rôle stable ✅ ← ce qu'on déploie + # transaction + DEALLOCATE ALL → rôle stable ✅ (par ACCIDENT : la fuite + # qu'on referme est ce qui le sauvait) + # transaction + DISCARD ALL → rôle ÉPHÉMÈRE ❌ le défaut du 28/07, + # qui a mis l'API à terre une demi-journée + # + # Et `poserRoleProprietaire` ne peut pas le voir : sa relecture de + # `current_role` a lieu à l'ouverture, où le rôle est encore correct. + # Ce qui couvre ce cas, c'est la ceinture Vault (`ALTER ROLE … SET ROLE`, + # module app_roles) : un défaut de rôle survit à `DISCARD ALL`. **Avant de + # passer en transaction pooling, vérifier que les baux Vault ont tourné.** server_reset_query: DISCARD ALL # défaut pgbouncer — ⚠ ne pas réduire : voir ci-dessus server_idle_timeout: 7200 pgbouncerExporter: