doc(pgbouncer) — la version mesurée n'était pas la déployée, et DISCARD ALL a un prix
Helm Charts / Detect changed charts (pull_request) Successful in 28s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped

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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GdUCA5Uz8QyMwa2P4Pg2hK
This commit is contained in:
2026-07-28 23:23:18 +02:00
co-authored by Claude Opus 5
parent 1dd905dc5f
commit 7d13a8d764
+37 -4
View File
@@ -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: