997 rôles éphémères survivent dans pg_roles, dont 262 encore connectables — la révocation Vault ne révoque rien #26

Open
opened 2026-07-28 22:52:16 +02:00 by arcodange · 0 comments
Owner

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 :

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_statementsconditionné à 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).
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: arcodange-org/tools#26