SELECT split_part(rolname,'-',3) AS app, count(*)
FROM pg_roles WHERE rolname LIKE 'v-kubernet-%' GROUP BY 1;
app
rôles survivants
erp
299
crowdsec
240
plausible
235
webapp
212
kadans
11
total
997, dont 262 dont le VALID UNTIL n'est pas encore passé
Chacun est membre de son <app>_role — vérifié : les 11 rôles v-kubernet-kadans-% le sont tous.
Pourquoi ils survivent, et pourquoi la révocation ne protège pas
hashicorp-vault/iac/modules/app_roles/main.tf :
revocation_statements=["REASSIGN OWNED BY \"{{name}}\" TO ${local.owner_role};", # ne tourne PAS dans la bonne base
"REVOKE ALL ON DATABASE ${local.database} FROM \"{{name}}\";", # should we drop the role ? -> YES
]
Le rôle n'est jamais DROPé — le TODO du fichier l'avoue.
Le REVOKE ALL ON DATABASE ne ferme pas la porte. Mesuré :
kadans | datacl = NULL (=> PUBLIC a CONNECT par défaut)
erp | datacl = NULL
crowdsec | datacl = NULL
plausible | datacl = NULL
webapp | datacl = {=Tc/webapp_role, webapp_role=CTc/webapp_role} ← la seule base fermée
Sur quatre bases sur cinq, datacl est NULL : PUBLIC a CONNECT. Retirer le privilège au rôle ne change donc rien — il se reconnecte par PUBLIC, avec son mot de passe encore valide et son appartenance à <app>_role intacte.
Conséquence : un identifiant dont le bail Vault est terminé reste pleinement utilisable jusqu'à son VALID UNTIL, soit ~30 jours. 262 le sont aujourd'hui. C'est ce qui transforme une fuite de log ou un dump de secret périmé en accès réel.
Ce qu'il faudrait
DROP ROLE "{{name}}"; en fin de revocation_statements — conditionné à ce que le REASSIGN fonctionne d'abord (un DROP échoue si le rôle possède encore des objets ; voir l'issue jumelle sur le REASSIGN).
Purger les 997 existants (DROP ROLE en boucle, après REASSIGN OWNED dans chaque base).
Envisager REVOKE CONNECT ON DATABASE <db> FROM PUBLIC sur les quatre bases ouvertes, en s'alignant sur ce que webapp fait déjà. ⚠ à valider par app : c'est ce qui a fermé webapp, ça peut casser un client qui se connecte hors du rôle attendu.
Repéré en refermant la fuite de session pgbouncer (#25).
## Mesuré le 2026-07-28
```
SELECT split_part(rolname,'-',3) AS app, count(*)
FROM pg_roles WHERE rolname LIKE 'v-kubernet-%' GROUP BY 1;
```
| app | rôles survivants |
|---|---|
| erp | 299 |
| crowdsec | 240 |
| plausible | 235 |
| webapp | 212 |
| kadans | 11 |
| **total** | **997**, dont **262 dont le `VALID UNTIL` n'est pas encore passé** |
Chacun est **membre de son `<app>_role`** — vérifié : les 11 rôles `v-kubernet-kadans-%` le sont tous.
## Pourquoi ils survivent, et pourquoi la révocation ne protège pas
`hashicorp-vault/iac/modules/app_roles/main.tf` :
```hcl
revocation_statements = [
"REASSIGN OWNED BY \"{{name}}\" TO ${local.owner_role};", # ne tourne PAS dans la bonne base
"REVOKE ALL ON DATABASE ${local.database} FROM \"{{name}}\";", # should we drop the role ? -> YES
]
```
1. **Le rôle n'est jamais `DROP`é** — le `TODO` du fichier l'avoue.
2. **Le `REVOKE ALL ON DATABASE` ne ferme pas la porte.** Mesuré :
```
kadans | datacl = NULL (=> PUBLIC a CONNECT par défaut)
erp | datacl = NULL
crowdsec | datacl = NULL
plausible | datacl = NULL
webapp | datacl = {=Tc/webapp_role, webapp_role=CTc/webapp_role} ← la seule base fermée
```
Sur quatre bases sur cinq, `datacl` est `NULL` : **PUBLIC a `CONNECT`**. Retirer le privilège au rôle ne change donc rien — il se reconnecte par PUBLIC, avec son mot de passe encore valide et son appartenance à `<app>_role` intacte.
**Conséquence** : un identifiant dont le bail Vault est terminé reste **pleinement utilisable** jusqu'à son `VALID UNTIL`, soit ~30 jours. 262 le sont aujourd'hui. C'est ce qui transforme une fuite de log ou un dump de secret périmé en accès réel.
## Ce qu'il faudrait
- `DROP ROLE "{{name}}";` en fin de `revocation_statements` — **conditionné** à ce que le `REASSIGN` fonctionne d'abord (un `DROP` échoue si le rôle possède encore des objets ; voir l'issue jumelle sur le `REASSIGN`).
- Purger les 997 existants (`DROP ROLE` en boucle, après `REASSIGN OWNED` dans chaque base).
- Envisager `REVOKE CONNECT ON DATABASE <db> FROM PUBLIC` sur les quatre bases ouvertes, en s'alignant sur ce que `webapp` fait déjà. ⚠ à valider par app : c'est ce qui a fermé `webapp`, ça peut casser un client qui se connecte hors du rôle attendu.
Repéré en refermant la fuite de session pgbouncer (#25).
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.
Mesuré le 2026-07-28
VALID UNTILn'est pas encore passéChacun est membre de son
<app>_role— vérifié : les 11 rôlesv-kubernet-kadans-%le sont tous.Pourquoi ils survivent, et pourquoi la révocation ne protège pas
hashicorp-vault/iac/modules/app_roles/main.tf:DROPé — leTODOdu fichier l'avoue.REVOKE ALL ON DATABASEne ferme pas la porte. Mesuré :Sur quatre bases sur cinq,
dataclestNULL: PUBLIC aCONNECT. Retirer le privilège au rôle ne change donc rien — il se reconnecte par PUBLIC, avec son mot de passe encore valide et son appartenance à<app>_roleintacte.Conséquence : un identifiant dont le bail Vault est terminé reste pleinement utilisable jusqu'à son
VALID UNTIL, soit ~30 jours. 262 le sont aujourd'hui. C'est ce qui transforme une fuite de log ou un dump de secret périmé en accès réel.Ce qu'il faudrait
DROP ROLE "{{name}}";en fin derevocation_statements— conditionné à ce que leREASSIGNfonctionne d'abord (unDROPéchoue si le rôle possède encore des objets ; voir l'issue jumelle sur leREASSIGN).DROP ROLEen boucle, aprèsREASSIGN OWNEDdans chaque base).REVOKE CONNECT ON DATABASE <db> FROM PUBLICsur les quatre bases ouvertes, en s'alignant sur ce quewebappfait déjà. ⚠ à valider par app : c'est ce qui a ferméwebapp, ça peut casser un client qui se connecte hors du rôle attendu.Repéré en refermant la fuite de session pgbouncer (#25).