Author SHA1 Message Date
arcodangeandClaude Opus 5 7d13a8d764 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
2026-07-28 23:23:18 +02:00
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
arcodange 9de9a9663b Merge pull request 'ci — arrêter les runs qui ne peuvent pas aboutir, et le doublon push+PR' (#23) from arcodange/moins-de-runs-inutiles into main
Helm Charts / Detect changed charts (push) Successful in 18s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-26 12:39:13 +02:00
arcodangeandClaude Opus 5 2bdc486ae6 ci — arrêter les runs qui ne peuvent pas aboutir, et le doublon push+PR
Helm Charts / Detect changed charts (pull_request) Successful in 17s
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Quatre runs d'une branche déjà mergée ont bloqué, ce matin, l'apply qu'on
attendait. Deux causes, indépendantes :

1. Les workflows tofu (minio, vault, crowdsec, plausible) s'authentifient à
   Vault par un flux OIDC dont un HUMAIN doit ouvrir le lien. Déclenchés tout
   seuls, ils ne peuvent qu'occuper un runner jusqu'au timeout. Ils font en
   plus `apply` en `auto_approve` CONTRE LA PROD : partir sur le push d'une
   branche, c'est appliquer du code que personne n'a relu. → `workflow_dispatch`
   seul, ce qui écrit enfin ce qu'ils faisaient déjà.

   (crowdsec et plausible passaient de toute façon par une ancre YAML, donc
   leurs triggers étaient INERTES — issues 113 → 117 de kadans.)

2. `push` sur toutes les branches + `pull_request` = DEUX runs par commit dès
   qu'une branche a une PR. Vérifié : runs 258/259 et 260/261 portent le même
   SHA. helmcharts, qui travaille seul et mérite de rester automatique, prend
   la forme éprouvée de la CI de kadans : push sur `main`, PR pour la branche.

Chaque clé de trigger porte un corps explicite : un `pull_request:` nu n'est
pas une forme éprouvée ici, et son mode d'échec est le silencieux.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-26 12:37:46 +02:00
arcodange 901aa9a9dc Merge pull request 'fix(minio) — le provisionneur ne pouvait pas créer de bucket : s3:ListBucket manquait' (#22) from arcodange/provisioner-listbucket into main
Helm Charts / Detect changed charts (push) Successful in 1m0s
Helm Charts / Library charts tool (push) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
MinIO / Auth with gitea for vault (push) Successful in 23s
MinIO / Tofu - minio IAC (push) Successful in 49s
2026-07-26 11:44:18 +02:00
arcodangeandClaude Opus 5 e8ab19962b fix(minio) — le provisionneur ne pouvait pas créer de bucket : s3:ListBucket manquait
Helm Charts / Detect changed charts (push) Successful in 15s
Helm Charts / Detect changed charts (pull_request) Successful in 16s
Helm Charts / Library charts tool (push) Has been skipped
MinIO / Auth with gitea for vault (push) Failing after 8m31s
MinIO / Tofu - minio IAC (push) Has been skipped
Helm Charts / Library charts tool (pull_request) Has been skipped
Helm Charts / Application charts pgcat (push) Has been skipped
MinIO / Auth with gitea for vault (pull_request) Failing after 8m32s
MinIO / Tofu - minio IAC (pull_request) Has been skipped
Helm Charts / Application charts pgcat (pull_request) Has been skipped
Le premier apply réel (kadans) a créé la politique, le compte de service,
l'attachement et le secret Vault — puis a échoué sur le bucket lui-même :

    Error: [FATAL] unable to check bucket (kadans-videos): Access Denied.

Le provider teste l'existence du bucket AVANT de le créer (HeadBucket), et
MinIO exige `s3:ListBucket` pour ça. C'est le seul point que l'ADR annonçait
comme non éprouvé ; il l'est maintenant.

`s3:ListBucket` donne la vue des CLÉS d'un bucket, pas leur contenu :
GetObject / PutObject restent absents, donc le provisionneur — partagé entre
les rôles CI — ne peut toujours pas lire les vidéos d'une autre application.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-26 11:43:04 +02:00
arcodange 5c7dce96e1 Merge pull request 'feat(minio) — un compte de service par app consommatrice, borné à son bucket' (#21) from arcodange/minio-comptes-de-service into main
Helm Charts / Detect changed charts (push) Successful in 19s
Hashicorp Vault / Auth with gitea for vault (push) Successful in 2m58s
MinIO / Auth with gitea for vault (push) Successful in 5m52s
Helm Charts / Library charts tool (push) Has been skipped
Hashicorp Vault / Tofu - Vault IAC (push) Successful in 53s
MinIO / Tofu - minio IAC (push) Successful in 33s
Helm Charts / Application charts pgcat (push) Has been skipped
2026-07-26 10:46:39 +02:00
8 changed files with 157 additions and 50 deletions
+8 -5
View File
@@ -2,12 +2,15 @@
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml # template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
name: Crowdsec name: Crowdsec
on: #[push,pull_request] # À LA DEMANDE, et seulement à la demande — comme minio.yaml : auth Vault par
# flux OIDC (un humain doit ouvrir un lien) et apply `auto_approve` contre la prod.
#
# Note : les triggers `push`/`pull_request` retirés ici étaient de toute façon
# INERTES — ils passaient par une ancre YAML (`&`/`*`), que le parseur
# d'événements de Gitea ne résout pas (vécu sur arcodange/kadans, issues 113
# → 117). Ce workflow ne partait déjà qu'à la main ; c'est maintenant écrit.
on:
workflow_dispatch: {} workflow_dispatch: {}
push: &crowdsecPaths
paths:
- 'crowdsec/**/*.tf'
pull_request: *crowdsecPaths
# cancel any previously-started, yet still active runs of this workflow on the same branch # cancel any previously-started, yet still active runs of this workflow on the same branch
concurrency: concurrency:
+26 -8
View File
@@ -2,14 +2,32 @@
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml # template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
name: Helm Charts name: Helm Charts
on: [push,pull_request,workflow_dispatch] # Celui-ci travaille SEUL (pas d'auth Vault, pas d'apply) : on le garde
# push: &helmPaths # turns out gitea don't handle well the paths filter # automatique. Mais `push` sur TOUTES les branches + `pull_request` faisait
# paths: # partir DEUX runs pour le même commit dès qu'une branche avait une PR.
# - '*/\.yaml' #
# - '*/\.tpl' # Même forme que la CI de kadans : la branche est couverte par `pull_request`,
# - '*/NOTES.txt' # `main` par le `push` d'après-merge. Un run par événement, aucun angle mort.
# - '*/\.helmignore' #
# pull_request: *helmPaths # (Le filtre de chemins d'origine, resté en commentaire des années sous un
# « gitea don't handle well the paths filter », n'était probablement pas en
# cause : il passait par une ancre YAML, et le parseur d'événements de Gitea ne
# les résout pas — issues 113 → 117 de kadans. Le job `filter-chart` fait déjà
# ce tri au niveau job, donc on n'y retouche pas.)
#
# ⚠ Chaque clé porte un CORPS explicite : un `pull_request:` nu (valeur nulle)
# n'est pas une forme éprouvée sur ce Gitea, et son mode d'échec est le
# silencieux — aucun run, aucune erreur. On copie la forme qui tourne (kadans
# ci.yml), listes dupliquées à la main, sans ancre.
on:
workflow_dispatch: {}
push:
branches: [main]
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
# cancel any previously-started, yet still active runs of this workflow on the same branch # cancel any previously-started, yet still active runs of this workflow on the same branch
concurrency: concurrency:
+11 -12
View File
@@ -1,20 +1,19 @@
--- ---
name: MinIO name: MinIO
# ⚠ Triggers écrits EN TOUTES LETTRES, sans ancre YAML (`&`/`*`). # À LA DEMANDE, et seulement à la demande. Deux raisons, chacune suffisante :
# Une ancre dans un trigger Gitea Actions fait taire push ET pull_request — #
# en silence, aucun run, aucune erreur (vécu sur arcodange/kadans, issues 113 # 1. Ce workflow ne PEUT PAS aboutir sans un humain : l'auth Vault passe par
# → 117). Les autres workflows de ce repo utilisent encore des ancres : à # un flux OIDC dont le lien doit être ouvert dans un navigateur connecté.
# vérifier séparément, c'est probablement pour ça qu'ils ne partent qu'à la # Déclenché tout seul, il occupe un runner jusqu'à son timeout — et retarde
# main (workflow_dispatch). # les runs que quelqu'un attend vraiment.
# 2. Il fait `terraform apply` en `auto_approve` CONTRE LA PROD. Se déclencher
# sur le push d'une branche, c'est appliquer du code que personne n'a relu.
#
# Au passage : `push` (toutes branches) + `pull_request` faisait partir DEUX runs
# par commit d'une branche en PR — le même SHA, deux fois.
on: on:
workflow_dispatch: {} workflow_dispatch: {}
push:
paths:
- 'minio/**/*.tf'
pull_request:
paths:
- 'minio/**/*.tf'
# cancel any previously-started, yet still active runs of this workflow on the same branch # cancel any previously-started, yet still active runs of this workflow on the same branch
concurrency: concurrency:
+7 -5
View File
@@ -2,12 +2,14 @@
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml # template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
name: Plausible name: Plausible
on: #[push,pull_request] # À LA DEMANDE, et seulement à la demande — comme minio.yaml : auth Vault par
# flux OIDC (un humain doit ouvrir un lien) et apply `auto_approve` contre la prod.
#
# Note : les triggers `push`/`pull_request` retirés ici étaient de toute façon
# INERTES (ancre YAML non résolue par Gitea, issues 113 → 117 de kadans). Ce
# workflow ne partait déjà qu'à la main ; c'est maintenant écrit.
on:
workflow_dispatch: {} workflow_dispatch: {}
push: &plausiblePaths
paths:
- 'plausible/**/*.tf'
pull_request: *plausiblePaths
# cancel any previously-started, yet still active runs of this workflow on the same branch # cancel any previously-started, yet still active runs of this workflow on the same branch
concurrency: concurrency:
+10 -15
View File
@@ -2,23 +2,18 @@
# template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml # template source: https://github.com/bretfisher/docker-build-workflow/blob/main/templates/call-docker-build.yaml
name: Hashicorp Vault name: Hashicorp Vault
# ⚠ Triggers écrits EN TOUTES LETTRES, sans ancre YAML (`&`/`*`) : une ancre # À LA DEMANDE, et seulement à la demande — comme minio.yaml, et pour les mêmes
# dans un trigger Gitea Actions fait taire push ET pull_request, en silence # deux raisons : l'auth Vault exige qu'un humain ouvre un lien OIDC (sans lui, le
# (vécu sur arcodange/kadans, issues 113 → 117) — c'est probablement pourquoi # run squatte un runner jusqu'au timeout), et l'apply se fait en `auto_approve`
# ce workflow ne partait qu'à la main. # contre la prod.
# Et `*.tfvars` compte AUTANT que `*.tf` : la liste des applications (donc les #
# rôles gitea_cicd_<app>) vit dans terraform.tfvars — l'oublier, c'est ajouter # ⚠ Ce qui change AUSSI de nature : `hashicorp-vault/**/*.tfvars` compte autant
# une app sans jamais créer son rôle. # que `*.tf` — la liste des applications (donc les rôles gitea_cicd_<app>) vit
# dans terraform.tfvars. Ce n'est plus un filtre de chemins mais ça reste vrai
# du POURQUOI on relance : ajouter une app au tfvars sans relancer ce workflow,
# c'est une app sans rôle CI.
on: on:
workflow_dispatch: {} workflow_dispatch: {}
push:
paths:
- 'hashicorp-vault/**/*.tf'
- 'hashicorp-vault/**/*.tfvars'
pull_request:
paths:
- 'hashicorp-vault/**/*.tf'
- 'hashicorp-vault/**/*.tfvars'
# cancel any previously-started, yet still active runs of this workflow on the same branch # cancel any previously-started, yet still active runs of this workflow on the same branch
concurrency: concurrency:
@@ -30,9 +30,36 @@ resource "vault_database_secret_backend_role" "role" {
backend = local.vault_mount_postgres.path backend = local.vault_mount_postgres.path
name = local.instance name = local.instance
db_name = "postgres" db_name = "postgres"
# ── Le rôle ÉPHÉMÈRE endosse le rôle STABLE, dès le login ───────────────────
#
# En PostgreSQL, un objet appartient au rôle qui l'a CRÉÉ. Comme chaque
# démarrage de pod obtient un rôle `v-kubernet-…` neuf, toute migration crée
# des objets que le pod SUIVANT ne peut plus lire. C'est ce qui a mis l'API
# kadans à terre une demi-journée le 2026-07-28 (« permission denied for table
# qualification_video »), et c'est ce que le CronJob `pg-fix-table-ownership`
# rattrape tous les jours à 03:00 — a posteriori, et pour les seules TABLES.
#
# `ALTER ROLE … SET ROLE` fait de l'endossement un DÉFAUT DE CONNEXION : plus
# rien à poser côté application, et ça vaut aussi pour les clients qui ne sont
# pas l'application (le `psql` d'un job, une console d'exploitation).
#
# 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. Endosser une casquette qu'on porte
# déjà, ce n'est pas une élévation — et c'est pourquoi cette instruction ne
# peut pas échouer là où le `GRANT` réussit.
#
# MESURÉ (PostgreSQL 16, compte CREATEROLE non-superutilisateur, comme celui
# de Vault) : l'instruction passe, le login donne `session_user=v-test-1` /
# `current_role=proprio_v`, et un `ALTER ROLE … RESET role` la retire.
# Vérifié aussi qu'elle SURVIT à `RESET ALL` / `DISCARD ALL` — donc elle
# compose avec le `server_reset_query` de pgbouncer au lieu de s'y opposer.
#
# ⚠ Ne vaut que pour les identifiants créés APRÈS l'apply : les baux en cours
# gardent leur ancien comportement jusqu'à leur renouvellement.
creation_statements = [ creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';", "CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT ${local.owner_role} TO \"{{name}}\";", "GRANT ${local.owner_role} TO \"{{name}}\";",
"ALTER ROLE \"{{name}}\" SET ROLE ${local.owner_role};",
] ]
revocation_statements = [ revocation_statements = [
"REASSIGN OWNED BY \"{{name}}\" TO ${local.owner_role};", # reassign must be executed in the database where the reassgined objects are - TODO (one connection per database/app) "REASSIGN OWNED BY \"{{name}}\" TO ${local.owner_role};", # reassign must be executed in the database where the reassgined objects are - TODO (one connection per database/app)
+11 -4
View File
@@ -3,8 +3,9 @@
# qu'elles déclarent leurs buckets sans qu'on leur confie le root. # qu'elles déclarent leurs buckets sans qu'on leur confie le root.
# Décisions : factory/doc/adr/20260726-stockage-objet-minio.md # Décisions : factory/doc/adr/20260726-stockage-objet-minio.md
# #
# Noms d'actions issus de la documentation MinIO, NON éprouvés contre le # Noms d'actions confirmés par le premier apply réel (kadans, 2026-07-26) : le
# serveur : le premier apply les confirmera ou les corrigera. # bloc admin passe tel quel ; côté s3 il manquait `s3:ListBucket`, que le
# provider appelle AVANT de créer un bucket pour savoir s'il existe déjà.
resource "minio_iam_policy" "provisioner" { resource "minio_iam_policy" "provisioner" {
name = "provisioner" name = "provisioner"
policy = jsonencode({ policy = jsonencode({
@@ -26,9 +27,15 @@ resource "minio_iam_policy" "provisioner" {
Resource = ["arn:aws:s3:::*"] Resource = ["arn:aws:s3:::*"]
}, },
{ {
# s3:GetObject / s3:PutObject volontairement ABSENTS. # s3:GetObject / s3:PutObject volontairement ABSENTS : le provisionneur
# ne LIT ni n'ÉCRIT aucun objet, c'est ce qui rend son partage entre
# rôles CI acceptable.
#
# `s3:ListBucket` est la seule concession : MinIO le demande pour un
# HeadBucket, et le provider teste l'existence du bucket avant de le
# créer. Il donne la vue des CLÉS d'un bucket, jamais leur contenu.
Effect = "Allow" Effect = "Allow"
Action = ["s3:CreateBucket", "s3:DeleteBucket", "s3:ListAllMyBuckets", "s3:GetBucketLocation", "s3:GetBucketPolicy", "s3:PutBucketPolicy"] Action = ["s3:CreateBucket", "s3:DeleteBucket", "s3:ListBucket", "s3:ListAllMyBuckets", "s3:GetBucketLocation", "s3:GetBucketPolicy", "s3:PutBucketPolicy"]
Resource = ["arn:aws:s3:::*"] Resource = ["arn:aws:s3:::*"]
}, },
] ]
+57 -1
View File
@@ -14,7 +14,63 @@ pgbouncer: &pgbouncer_config
auth_type: scram-sha-256 auth_type: scram-sha-256
auth_query: SELECT uname, phash FROM user_lookup($1) auth_query: SELECT uname, phash FROM user_lookup($1)
ignore_startup_parameters: extra_float_digits # unsupported jdbc extra_float_digits=2 argument ignore_startup_parameters: extra_float_digits # unsupported jdbc extra_float_digits=2 argument
server_reset_query: DEALLOCATE ALL # fix prepared statement already exist (crowdsec) # Ce pgbouncer est PARTAGÉ (crowdsec, plausible, kadans, + le compte que
# Vault utilise sur la base `postgres`). Ce qu'un client laisse derrière
# lui sur une connexion serveur, le client SUIVANT en hérite : le reset
# est la SEULE barrière entre deux clients d'un même pool.
#
# `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.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`
# (donc le correctif crowdsec est conservé — vérifié : deux clients
# successifs préparent le même nom sans erreur) ET
# `SELECT pg_advisory_unlock_all()` (la valeur que pose le sous-chart).
#
# Il ne peut RIEN casser pour un client vivant : en `pool_mode = session`
# 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 server_idle_timeout: 7200
pgbouncerExporter: pgbouncerExporter:
enabled: false enabled: false