6 Commits
Author SHA1 Message Date
arcodangeandClaude Opus 5 1dd905dc5f fix(pgbouncer,vault) — ce qu'un client laisse sur une connexion, le suivant n'en hérite plus
Helm Charts / Detect changed charts (pull_request) Successful in 58s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
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
2026-07-28 22:48:49 +02:00
arcodangeandClaude Opus 4.7 a3e121b468 modules: add env/envs parameter to app_roles + app_policy (multi-env)
Helm Charts / Detect changed charts (push) Successful in 23s
Helm Charts / Detect changed charts (pull_request) Successful in 22s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Phase A of the multi-environment evolution agreed in the erp repo design
thread. Both modules gain an optional env coordinate that defaults to
"prod"; by the elision rule, env=prod produces the existing single-env
derived names character-for-character, so every existing app's tofu plan
is a no-op.

app_roles (per-instance module — caller iterates over envs):
- variables.tf: add optional env = "prod"
- main.tf: compute local.instance via elision rule + local.owner_role
  (snake-case <name>_<env>_role for the Postgres owner). The name/env/
  database locals are grouped so fmt keeps the existing `name` alignment
  (no whitespace churn on unchanged keys).
- main.tf: substitute local.name -> local.instance / local.owner_role in
  the dynamic role name, k8s role name, SA bindings, token_policies
- outputs.tf: add env + instance outputs; kvv2_path_prefix derives from
  local.instance (== local.name when env=prod → backwards-compat)

app_policy (per-repo module — accepts list of envs):
- variables.tf: add optional envs = ["prod"]
- main.tf: compute local.instances + local.non_prod_instances; remove the
  now-dead bound_service_account_* alias locals (the allowed_parameter
  blocks build their values from per_instance_sa_* maps instead)
- main.tf: kvv2 ops rules become dynamic blocks iterating local.instances
  in the original order (data, delete, undelete, destroy, metadata), so a
  prod-only app renders a byte-identical policy document
- main.tf: allowed_parameter for bound_service_account_* + token_policies
  use comprehensions over local.instances (1-element → identical to old
  static values for prod-only apps)
- main.tf: keep vault_policy.app (env=prod runtime policy) at its original
  address; add vault_policy.app_non_prod via for_each over non_prod_instances
  (empty set for prod-only apps → no new resources)

Top-level wiring:
- iac/variables.tf: add envs = optional(list(string), ["prod"]) to the
  applications set(object) type
- iac/main.tf: pass envs = each.value.envs to app_policies

Verified: `tofu fmt -check` clean on all touched files, `tofu validate`
passes. Backwards-compat reasoning for the no-op plan is in the PR body.

Phase B (factory postgres iac + argocd + runbook docs) and Phase D
(erp iac/main.tf for_each + activate sandbox) follow in their own PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-15 14:29:11 +02:00
arcodange a5338ac6f7 TODO: 1 vault_database_secret_backend_connection per database 2025-12-09 12:58:59 +01:00
arcodange 50f8ea95be allow several k8s SA to take an app policy
Helm Charts / Detect changed charts (push) Successful in 15s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2025-11-27 23:24:47 +01:00
arcodange ea9e41ff1a tofu fmt -recursive and kvv1/cloudflare permission for cms project
Helm Charts / Detect changed charts (push) Successful in 55s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2025-10-24 18:00:16 +02:00
arcodange 784c014224 add app_roles terraform module for vault and declare erp app 2024-11-08 19:01:22 +01:00