Le pgbouncer du namespace tools est partagé (crowdsec, plausible, kadans, + le compte que Vault utilise sur la base postgres). La seule barrière entre deux clients d'un même pool est le server_reset_query — et la ConfigMap écrase le défaut de pgbouncer par DEALLOCATE ALL, qui ne nettoie que les requêtes préparées.
Tout le reste de l'état de session traverse la déconnexion et tombe chez le client suivant.
Mesuré, pas supposé
pgbouncer 1.25.2 (la version en prod), monté avec notre configuration : pool_mode = session, server_reset_query_always = 1, default_pool_size = 1. Le client 1 pose son état puis se déconnecte ; le client 2, qui n'a rien demandé, arrive.
server_reset_query
ce dont hérite le client 2
DEALLOCATE ALL (l'existant)
work_mem = 17MB, 1 LISTEN, la table TEMP du client 1, et son SET ROLE — au point de créer des tables appartenant à un rôle qui n'est pas le sien
DISCARD ALL (ce PR)
work_mem = 4MB, 0 LISTEN, pas de table TEMP, son propre rôle
Pourquoi DISCARD ALL ne peut rien casser
C'est le défaut de pgbouncer, et un sur-ensemble strict des deux valeurs qui l'ont précédé ici : il contient DEALLOCATE ALL — donc le correctif crowdsec de 07e2c6d (« prepared statement already exists ») est conservé, vérifié : deux clients successifs préparent le même nom sans erreur — et il contient SELECT pg_advisory_unlock_all(), la valeur que pose le sous-chart.
Il ne touche jamais un client vivant : en pool_mode = session la connexion serveur n'est rendue qu'à la déconnexion, donc le reset ne court pas entre deux requêtes d'une même session. Vérifié : table TEMP, SET work_mem et LISTEN d'un client vivant survivent intacts.
Portée de la fuite — ce qu'elle est, et ce qu'elle n'est pas
Les pools sont partitionnés par (base, utilisateur) : 210 pools mesurés sur l'instance, aucun ne mélange deux bases ni deux comptes. Un rôle posé par kadans ne peut pas atterrir chez crowdsec ou plausible : le seul héritier possible est un client de la même base avec le même identifiant, donc l'application elle-même.
Ce n'est donc pas une élévation de privilège entre applications. C'est un défaut d'hygiène — et il est déjà là aujourd'hui pour n'importe quel SET de n'importe quelle application.
La ceinture, côté Vault
ALTER ROLE "{{name}}" SET ROLE <app>_role dans les creation_statements fait de l'endossement un défaut de connexion : immune au pool_mode comme au server_reset_query, et valable pour les clients qui ne sont pas l'application (le psql d'un job).
Aucun privilège nouveau : le GRANT de la ligne précédente rend déjà le rôle éphémère membre du rôle stable. Elle ne peut pas échouer là où le GRANT réussit.
Elle règle à la racine ce que le CronJob pg-fix-table-ownership rattrape tous les jours à 03:00.
Mesuré (PostgreSQL 16, compte CREATEROLEnon-superutilisateur, comme celui de Vault) : l'instruction passe, le login donne current_role = rôle stable avec session_user = rôle éphémère, ALTER ROLE … RESET role la retire, et elle survit à RESET ALL / DISCARD ALL — les deux mesures composent au lieu de s'annuler.
⚠ Elle ne vaut que pour les identifiants créés après l'apply. Elle ne protège pas ce soir ; c'est DISCARD ALL qui protège ce soir.
Ordre de déploiement
DISCARD ALL — ArgoCD + redémarrage du pod pgbouncer. C'est le maillon qui protège immédiatement.
La ceinture Vault — tofu apply du pipeline hashicorp-vault, puis elle entre en vigueur au fil du renouvellement des baux. Elle change le comportement de toutes les apps consommatrices du module (crowdsec, plausible, webapp, erp, kadans) à leur prochain apply, dans le sens que le CronJob de 03:00 leur applique déjà de force.
Ce que ce PR ne traite pas
#TODO du REASSIGN OWNED BY (il tourne dans la base postgres, pas dans celle de l'app) → issue séparée.
997 rôles éphémères survivants dans pg_roles, dont 262 encore valides → issue séparée.
Le CronJob pg-fix-table-ownership ne traite que pg_tables → issue séparée (angle mort déjà occupé : citext et pgcrypto de la base kadans appartiennent à un rôle éphémère).
Recette : tofu fmt -check vert sur le module, helm template du sous-chart rendu et vérifié (server_reset_query = DISCARD ALL en dernière position, celle qui gagne).
## Ce que ça referme
Le pgbouncer du namespace `tools` est **partagé** (crowdsec, plausible, kadans, + le compte que Vault utilise sur la base `postgres`). La seule barrière entre deux clients d'un même pool est le `server_reset_query` — et la ConfigMap écrase le défaut de pgbouncer par `DEALLOCATE ALL`, qui ne nettoie **que** les requêtes préparées.
Tout le reste de l'état de session traverse la déconnexion et tombe chez le client suivant.
## Mesuré, pas supposé
pgbouncer 1.25.2 (la version en prod), monté avec **notre** configuration : `pool_mode = session`, `server_reset_query_always = 1`, `default_pool_size = 1`. Le client 1 pose son état puis se déconnecte ; le client 2, qui n'a rien demandé, arrive.
| `server_reset_query` | ce dont hérite le client 2 |
|---|---|
| `DEALLOCATE ALL` (l'existant) | `work_mem = 17MB`, 1 `LISTEN`, la table TEMP du client 1, **et son `SET ROLE`** — au point de créer des tables appartenant à un rôle qui n'est pas le sien |
| `DISCARD ALL` (ce PR) | `work_mem = 4MB`, 0 `LISTEN`, pas de table TEMP, **son propre rôle** |
## Pourquoi `DISCARD ALL` ne peut rien casser
- C'est le **défaut de pgbouncer**, et un **sur-ensemble strict** des deux valeurs qui l'ont précédé ici : il contient `DEALLOCATE ALL` — donc le correctif crowdsec de 07e2c6d (« prepared statement already exists ») est **conservé**, vérifié : deux clients successifs préparent le même nom sans erreur — et il contient `SELECT pg_advisory_unlock_all()`, la valeur que pose le sous-chart.
- Il ne touche **jamais** un client vivant : en `pool_mode = session` la connexion serveur n'est rendue qu'à la déconnexion, donc le reset ne court pas entre deux requêtes d'une même session. Vérifié : table TEMP, `SET work_mem` et `LISTEN` d'un client **vivant** survivent intacts.
## Portée de la fuite — ce qu'elle est, et ce qu'elle n'est pas
Les pools sont partitionnés par **(base, utilisateur)** : 210 pools mesurés sur l'instance, aucun ne mélange deux bases ni deux comptes. Un rôle posé par kadans **ne peut pas** atterrir chez crowdsec ou plausible : le seul héritier possible est un client de la même base avec le même identifiant, donc l'application elle-même.
Ce n'est donc **pas** une élévation de privilège entre applications. C'est un défaut d'hygiène — et il est **déjà là aujourd'hui** pour n'importe quel `SET` de n'importe quelle application.
## La ceinture, côté Vault
`ALTER ROLE "{{name}}" SET ROLE <app>_role` dans les `creation_statements` fait de l'endossement un **défaut de connexion** : immune au `pool_mode` comme au `server_reset_query`, et valable pour les clients qui ne sont pas l'application (le `psql` d'un job).
- **Aucun privilège nouveau** : le `GRANT` de la ligne précédente rend déjà le rôle éphémère membre du rôle stable. Elle ne peut pas échouer là où le `GRANT` réussit.
- Elle règle **à la racine** ce que le CronJob `pg-fix-table-ownership` rattrape tous les jours à 03:00.
- Mesuré (PostgreSQL 16, compte `CREATEROLE` **non-superutilisateur**, comme celui de Vault) : l'instruction passe, le login donne `current_role` = rôle stable avec `session_user` = rôle éphémère, `ALTER ROLE … RESET role` la retire, et elle **survit à `RESET ALL` / `DISCARD ALL`** — les deux mesures composent au lieu de s'annuler.
- ⚠ Elle ne vaut que pour les identifiants créés **après** l'apply. **Elle ne protège pas ce soir** ; c'est `DISCARD ALL` qui protège ce soir.
## Ordre de déploiement
1. **`DISCARD ALL`** — ArgoCD + redémarrage du pod pgbouncer. C'est le maillon qui protège immédiatement.
2. **La ceinture Vault** — `tofu apply` du pipeline `hashicorp-vault`, puis elle entre en vigueur au fil du renouvellement des baux. Elle change le comportement de **toutes** les apps consommatrices du module (crowdsec, plausible, webapp, erp, kadans) à leur prochain apply, dans le sens que le CronJob de 03:00 leur applique déjà de force.
## Ce que ce PR ne traite pas
- `#TODO` du `REASSIGN OWNED BY` (il tourne dans la base `postgres`, pas dans celle de l'app) → issue séparée.
- 997 rôles éphémères survivants dans `pg_roles`, dont **262 encore valides** → issue séparée.
- Le CronJob `pg-fix-table-ownership` ne traite que `pg_tables` → issue séparée (angle mort **déjà occupé** : `citext` et `pgcrypto` de la base `kadans` appartiennent à un rôle éphémère).
Recette : `tofu fmt -check` vert sur le module, `helm template` du sous-chart rendu et vérifié (`server_reset_query = DISCARD ALL` en dernière position, celle qui gagne).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01GdUCA5Uz8QyMwa2P4Pg2hK
Un pgbouncer partagé n'a qu'UNE barrière entre deux clients d'un même pool : le
`server_reset_query`. La nôtre ne nettoyait que les requêtes préparées. Tout le
reste de l'état de session — variables, LISTEN, tables TEMP, et le rôle
courant — traversait la déconnexion et tombait dans les mains du client suivant.
CE QUI A ÉTÉ MESURÉ, PAS SUPPOSÉ
pgbouncer 1.25.2 (la version qui tourne), monté avec NOTRE configuration :
`pool_mode = session`, `server_reset_query = DEALLOCATE ALL`,
`server_reset_query_always = 1`, `default_pool_size = 1`. Le client 1 pose son
état puis se déconnecte ; le client 2, qui n'a rien demandé, arrive.
DEALLOCATE ALL (l'existant) client 2 hérite : work_mem=17MB, 1 LISTEN,
la table TEMP du client 1, et son SET ROLE —
au point de créer des tables appartenant à un
rôle qui n'est pas le sien.
DISCARD ALL (ce commit) client 2 obtient work_mem=4MB, 0 LISTEN, pas
de table TEMP, et son PROPRE rôle.
POURQUOI `DISCARD ALL` NE PEUT RIEN CASSER
C'est le défaut de pgbouncer, et un sur-ensemble STRICT des deux valeurs qui
l'ont précédé ici : il contient `DEALLOCATE ALL` — donc le correctif crowdsec de
07e2c6d (« prepared statement already exists ») est conservé, et c'est vérifié :
deux clients successifs préparent le même nom sans erreur — et il contient
`SELECT pg_advisory_unlock_all()`, la valeur que pose le sous-chart.
Et il ne touche jamais un client vivant : en `pool_mode = session` la connexion
serveur n'est rendue qu'à la déconnexion, donc le reset ne court pas entre deux
requêtes d'une même session. Vérifié : table TEMP, `SET work_mem` et `LISTEN`
d'un client VIVANT survivent intacts.
PORTÉE DE LA FUITE — ce qu'elle est, et ce qu'elle n'est pas
Les pools de pgbouncer sont partitionnés par (base, utilisateur) : 210 pools
mesurés sur l'instance, aucun ne mélange deux bases ni deux comptes. Un rôle
posé par kadans ne peut donc PAS atterrir chez crowdsec ou plausible : le seul
héritier possible est un client de la même base avec le même identifiant, donc
l'application elle-même. Ce n'est pas une élévation de privilège entre
applications — c'est un défaut d'hygiène, et il est déjà là aujourd'hui pour
n'importe quel `SET` de n'importe quelle application.
LA CEINTURE, CÔTÉ VAULT
`ALTER ROLE "{{name}}" SET ROLE <app>_role` dans les creation_statements fait de
l'endossement un défaut de CONNEXION, immune au `pool_mode` comme au
`server_reset_query`, et valable pour les clients qui ne sont pas l'application
(le psql d'un job). Elle n'apporte aucun privilège : le `GRANT` de la ligne
précédente rend déjà le rôle éphémère membre du rôle stable — d'où le fait
qu'elle ne peut pas échouer là où le GRANT réussit.
Elle règle à la racine ce que le CronJob `pg-fix-table-ownership` rattrape tous
les jours à 03:00 : les objets naissent chez le rôle stable au lieu d'être
réattribués après coup. ⚠ Elle ne vaut que pour les identifiants créés APRÈS
l'apply — elle ne protège donc PAS ce soir ; c'est `DISCARD ALL` qui protège ce
soir.
Mesuré (PostgreSQL 16, compte CREATEROLE non-superutilisateur comme celui de
Vault) : l'instruction passe, le login donne bien `current_role` = rôle stable
avec `session_user` = rôle éphémère, un `ALTER ROLE … RESET role` la retire, et
elle SURVIT à `RESET ALL` / `DISCARD ALL` — les deux mesures composent au lieu
de s'annuler.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GdUCA5Uz8QyMwa2P4Pg2hK
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
Une contre-vérification a refait les mesures au lieu de les relire. Trois choses ne tenaient pas.
1. La version mesurée n'est pas celle qui tourne. Ce PR affirmait « pgbouncer 1.25.2 (la version en prod) ». Le pod exécute 1.23.1 (ghcr.io/icoretech/pgbouncer-docker:1.23.1-fixed, chart pgbouncer-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 manquait, donc elle se lisait au pire. Les pools sont partitionnés par (base, utilisateur) — 210 pools, aucun ne mélange deux bases ni deux comptes. Le seul héritier possible du SET ROLE de kadans-api est kadans-api elle-même. Aucune élévation vers crowdsec ou plausible n'est possible : c'est un défaut d'hygiène, pas une brèche inter-applications.
3. ⚠ Le corps de ce PR écrit « Il ne peut RIEN casser ». C'est faux sans qualificatif — et l'erreur est dans le sens inconfortable.
DISCARD ALL rend la protection de db.gostrictement plus fragile. Aujourd'hui (DEALLOCATE ALL), elle survivrait à un passage en transaction pooling ; après, non. Les quatre combinaisons, mesurées — propriétaire de la table créée :
pool_mode
reset query
propriétaire
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, celui qui a mis l'API à terre une demi-journée
Et poserRoleProprietairene peut pas le voir : sa relecture de current_role a lieu à l'ouverture, où le rôle est encore correct.
Sans danger aujourd'hui : pool_mode n'est pas déclaré dans la ConfigMap, donc il vaut session. Mais c'est exactement le genre de trappe qui se referme des mois plus tard sur quelqu'un qui optimise — l'avertissement est donc écrit au-dessus de la ligne, avec les quatre mesures. La ceinture Vault couvre ce cas (un défaut de rôle survit à DISCARD ALL), mais seulement au renouvellement des baux.
⚠ Ce PR attend une relecture humaine — il touche un pgbouncer partagé
kadans-api#48 est mergé : le SET ROLE protège la migration 0018 ce soir, indépendamment de ce PR. Il n'y a donc aucune urgence à merger celui-ci, et deux raisons de ne pas le faire sans toi :
il modifie un composant partagé par cinq applications (crowdsec, plausible, erp, webapp, kadans) ;
la ceinture Vault changera le comportement de ces cinq apps à leur prochain tofu apply.
L'ordre reste DISCARD ALL d'abord (ArgoCD + redémarrage du pod pgbouncer), la ceinture Vault ensuite.
## Correction du raisonnement — commit `7d13a8d`
Une contre-vérification a refait les mesures au lieu de les relire. Trois choses ne tenaient pas.
**1. La version mesurée n'est pas celle qui tourne.** Ce PR affirmait « pgbouncer 1.25.2 (la version en prod) ». Le pod exécute **1.23.1** (`ghcr.io/icoretech/pgbouncer-docker:1.23.1-fixed`, chart `pgbouncer-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 manquait, donc elle se lisait au pire.** Les pools sont partitionnés par **(base, utilisateur)** — 210 pools, aucun ne mélange deux bases ni deux comptes. Le seul héritier possible du `SET ROLE` de kadans-api est **kadans-api elle-même**. Aucune élévation vers crowdsec ou plausible n'est possible : c'est un **défaut d'hygiène, pas une brèche inter-applications**.
**3. ⚠ Le corps de ce PR écrit « Il ne peut RIEN casser ». C'est faux sans qualificatif — et l'erreur est dans le sens inconfortable.**
`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. Les quatre combinaisons, mesurées — propriétaire de la table créée :
| `pool_mode` | reset query | propriétaire |
|---|---|---|
| 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, celui 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.
**Sans danger aujourd'hui** : `pool_mode` n'est pas déclaré dans la ConfigMap, donc il vaut `session`. Mais c'est exactement le genre de trappe qui se referme des mois plus tard sur quelqu'un qui optimise — l'avertissement est donc écrit **au-dessus de la ligne**, avec les quatre mesures. La ceinture Vault couvre ce cas (un défaut de rôle survit à `DISCARD ALL`), mais seulement **au renouvellement des baux**.
---
### ⚠ Ce PR attend une relecture humaine — il touche un pgbouncer partagé
`kadans-api#48` est **mergé** : le `SET ROLE` protège la migration 0018 ce soir, indépendamment de ce PR. Il n'y a donc **aucune urgence** à merger celui-ci, et deux raisons de ne pas le faire sans toi :
- il modifie un composant partagé par **cinq applications** (crowdsec, plausible, erp, webapp, kadans) ;
- la ceinture Vault changera le comportement de ces cinq apps à leur prochain `tofu apply`.
L'ordre reste `DISCARD ALL` d'abord (ArgoCD + redémarrage du pod pgbouncer), la ceinture Vault ensuite.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Ce que ça referme
Le pgbouncer du namespace
toolsest partagé (crowdsec, plausible, kadans, + le compte que Vault utilise sur la basepostgres). La seule barrière entre deux clients d'un même pool est leserver_reset_query— et la ConfigMap écrase le défaut de pgbouncer parDEALLOCATE ALL, qui ne nettoie que les requêtes préparées.Tout le reste de l'état de session traverse la déconnexion et tombe chez le client suivant.
Mesuré, pas supposé
pgbouncer 1.25.2 (la version en prod), monté avec notre configuration :
pool_mode = session,server_reset_query_always = 1,default_pool_size = 1. Le client 1 pose son état puis se déconnecte ; le client 2, qui n'a rien demandé, arrive.server_reset_queryDEALLOCATE ALL(l'existant)work_mem = 17MB, 1LISTEN, la table TEMP du client 1, et sonSET ROLE— au point de créer des tables appartenant à un rôle qui n'est pas le sienDISCARD ALL(ce PR)work_mem = 4MB, 0LISTEN, pas de table TEMP, son propre rôlePourquoi
DISCARD ALLne peut rien casserDEALLOCATE ALL— donc le correctif crowdsec de07e2c6d(« prepared statement already exists ») est conservé, vérifié : deux clients successifs préparent le même nom sans erreur — et il contientSELECT pg_advisory_unlock_all(), la valeur que pose le sous-chart.pool_mode = sessionla connexion serveur n'est rendue qu'à la déconnexion, donc le reset ne court pas entre deux requêtes d'une même session. Vérifié : table TEMP,SET work_memetLISTENd'un client vivant survivent intacts.Portée de la fuite — ce qu'elle est, et ce qu'elle n'est pas
Les pools sont partitionnés par (base, utilisateur) : 210 pools mesurés sur l'instance, aucun ne mélange deux bases ni deux comptes. Un rôle posé par kadans ne peut pas atterrir chez crowdsec ou plausible : le seul héritier possible est un client de la même base avec le même identifiant, donc l'application elle-même.
Ce n'est donc pas une élévation de privilège entre applications. C'est un défaut d'hygiène — et il est déjà là aujourd'hui pour n'importe quel
SETde n'importe quelle application.La ceinture, côté Vault
ALTER ROLE "{{name}}" SET ROLE <app>_roledans lescreation_statementsfait de l'endossement un défaut de connexion : immune aupool_modecomme auserver_reset_query, et valable pour les clients qui ne sont pas l'application (lepsqld'un job).GRANTde la ligne précédente rend déjà le rôle éphémère membre du rôle stable. Elle ne peut pas échouer là où leGRANTréussit.pg-fix-table-ownershiprattrape tous les jours à 03:00.CREATEROLEnon-superutilisateur, comme celui de Vault) : l'instruction passe, le login donnecurrent_role= rôle stable avecsession_user= rôle éphémère,ALTER ROLE … RESET rolela retire, et elle survit àRESET ALL/DISCARD ALL— les deux mesures composent au lieu de s'annuler.DISCARD ALLqui protège ce soir.Ordre de déploiement
DISCARD ALL— ArgoCD + redémarrage du pod pgbouncer. C'est le maillon qui protège immédiatement.tofu applydu pipelinehashicorp-vault, puis elle entre en vigueur au fil du renouvellement des baux. Elle change le comportement de toutes les apps consommatrices du module (crowdsec, plausible, webapp, erp, kadans) à leur prochain apply, dans le sens que le CronJob de 03:00 leur applique déjà de force.Ce que ce PR ne traite pas
#TODOduREASSIGN OWNED BY(il tourne dans la basepostgres, pas dans celle de l'app) → issue séparée.pg_roles, dont 262 encore valides → issue séparée.pg-fix-table-ownershipne traite quepg_tables→ issue séparée (angle mort déjà occupé :citextetpgcryptode la basekadansappartiennent à un rôle éphémère).Recette :
tofu fmt -checkvert sur le module,helm templatedu sous-chart rendu et vérifié (server_reset_query = DISCARD ALLen dernière position, celle qui gagne).🤖 Generated with Claude Code
https://claude.ai/code/session_01GdUCA5Uz8QyMwa2P4Pg2hK
Un pgbouncer partagé n'a qu'UNE barrière entre deux clients d'un même pool : le `server_reset_query`. La nôtre ne nettoyait que les requêtes préparées. Tout le reste de l'état de session — variables, LISTEN, tables TEMP, et le rôle courant — traversait la déconnexion et tombait dans les mains du client suivant. CE QUI A ÉTÉ MESURÉ, PAS SUPPOSÉ pgbouncer 1.25.2 (la version qui tourne), monté avec NOTRE configuration : `pool_mode = session`, `server_reset_query = DEALLOCATE ALL`, `server_reset_query_always = 1`, `default_pool_size = 1`. Le client 1 pose son état puis se déconnecte ; le client 2, qui n'a rien demandé, arrive. DEALLOCATE ALL (l'existant) client 2 hérite : work_mem=17MB, 1 LISTEN, la table TEMP du client 1, et son SET ROLE — au point de créer des tables appartenant à un rôle qui n'est pas le sien. DISCARD ALL (ce commit) client 2 obtient work_mem=4MB, 0 LISTEN, pas de table TEMP, et son PROPRE rôle. POURQUOI `DISCARD ALL` NE PEUT RIEN CASSER C'est le défaut de pgbouncer, et un sur-ensemble STRICT des deux valeurs qui l'ont précédé ici : il contient `DEALLOCATE ALL` — donc le correctif crowdsec de07e2c6d(« prepared statement already exists ») est conservé, et c'est vérifié : deux clients successifs préparent le même nom sans erreur — et il contient `SELECT pg_advisory_unlock_all()`, la valeur que pose le sous-chart. Et il ne touche jamais un client vivant : en `pool_mode = session` la connexion serveur n'est rendue qu'à la déconnexion, donc le reset ne court pas entre deux requêtes d'une même session. Vérifié : table TEMP, `SET work_mem` et `LISTEN` d'un client VIVANT survivent intacts. PORTÉE DE LA FUITE — ce qu'elle est, et ce qu'elle n'est pas Les pools de pgbouncer sont partitionnés par (base, utilisateur) : 210 pools mesurés sur l'instance, aucun ne mélange deux bases ni deux comptes. Un rôle posé par kadans ne peut donc PAS atterrir chez crowdsec ou plausible : le seul héritier possible est un client de la même base avec le même identifiant, donc l'application elle-même. Ce n'est pas une élévation de privilège entre applications — c'est un défaut d'hygiène, et il est déjà là aujourd'hui pour n'importe quel `SET` de n'importe quelle application. LA CEINTURE, CÔTÉ VAULT `ALTER ROLE "{{name}}" SET ROLE <app>_role` dans les creation_statements fait de l'endossement un défaut de CONNEXION, immune au `pool_mode` comme au `server_reset_query`, et valable pour les clients qui ne sont pas l'application (le psql d'un job). Elle n'apporte aucun privilège : le `GRANT` de la ligne précédente rend déjà le rôle éphémère membre du rôle stable — d'où le fait qu'elle ne peut pas échouer là où le GRANT réussit. Elle règle à la racine ce que le CronJob `pg-fix-table-ownership` rattrape tous les jours à 03:00 : les objets naissent chez le rôle stable au lieu d'être réattribués après coup. ⚠ Elle ne vaut que pour les identifiants créés APRÈS l'apply — elle ne protège donc PAS ce soir ; c'est `DISCARD ALL` qui protège ce soir. Mesuré (PostgreSQL 16, compte CREATEROLE non-superutilisateur comme celui de Vault) : l'instruction passe, le login donne bien `current_role` = rôle stable avec `session_user` = rôle éphémère, un `ALTER ROLE … RESET role` la retire, et elle SURVIT à `RESET ALL` / `DISCARD ALL` — les deux mesures composent au lieu de s'annuler. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GdUCA5Uz8QyMwa2P4Pg2hKCorrection du raisonnement — commit
7d13a8dUne contre-vérification a refait les mesures au lieu de les relire. Trois choses ne tenaient pas.
1. La version mesurée n'est pas celle qui tourne. Ce PR affirmait « pgbouncer 1.25.2 (la version en prod) ». Le pod exécute 1.23.1 (
ghcr.io/icoretech/pgbouncer-docker:1.23.1-fixed, chartpgbouncer-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 manquait, donc elle se lisait au pire. Les pools sont partitionnés par (base, utilisateur) — 210 pools, aucun ne mélange deux bases ni deux comptes. Le seul héritier possible du
SET ROLEde kadans-api est kadans-api elle-même. Aucune élévation vers crowdsec ou plausible n'est possible : c'est un défaut d'hygiène, pas une brèche inter-applications.3. ⚠ Le corps de ce PR écrit « Il ne peut RIEN casser ». C'est faux sans qualificatif — et l'erreur est dans le sens inconfortable.
DISCARD ALLrend la protection dedb.gostrictement plus fragile. Aujourd'hui (DEALLOCATE ALL), elle survivrait à un passage en transaction pooling ; après, non. Les quatre combinaisons, mesurées — propriétaire de la table créée :pool_modeDEALLOCATE ALLDISCARD ALLDEALLOCATE ALLDISCARD ALLEt
poserRoleProprietairene peut pas le voir : sa relecture decurrent_rolea lieu à l'ouverture, où le rôle est encore correct.Sans danger aujourd'hui :
pool_moden'est pas déclaré dans la ConfigMap, donc il vautsession. Mais c'est exactement le genre de trappe qui se referme des mois plus tard sur quelqu'un qui optimise — l'avertissement est donc écrit au-dessus de la ligne, avec les quatre mesures. La ceinture Vault couvre ce cas (un défaut de rôle survit àDISCARD ALL), mais seulement au renouvellement des baux.⚠ Ce PR attend une relecture humaine — il touche un pgbouncer partagé
kadans-api#48est mergé : leSET ROLEprotège la migration 0018 ce soir, indépendamment de ce PR. Il n'y a donc aucune urgence à merger celui-ci, et deux raisons de ne pas le faire sans toi :tofu apply.L'ordre reste
DISCARD ALLd'abord (ArgoCD + redémarrage du pod pgbouncer), la ceinture Vault ensuite.