Files
tools/hashicorp-vault/iac/modules
arcodangeandClaude Opus 5 1dd905dc5f
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
fix(pgbouncer,vault) — ce qu'un client laisse sur une connexion, le suivant n'en hérite plus
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
..
2025-12-09 12:14:57 +01:00

Modules

app_policy

Ce module à déclarer dans ce projet permet au projet subordonné de déclarer le module app_roles suivant.

Ce module Terraform associe un projet Git à un ensemble de ressources Vault :

  • Une policy -ops pour la CI/CD du projet (dépôt Git).
  • Une policy app pour le runtime applicatif (pods).
  • Un groupe Vault lié au projet. (pour ajouter les utilisateurs vault associé à leur compte gitea)
  • Un rôle JWT Vault lié à ton SCM (ex: Gitea).
  • Les droits nécessaires pour gérer les rôles Kubernetes et Postgres associés au projet.

🚀 Usage

module "webapp_vault" {
  source = "./modules/vault_project"
  name   = "webapp"

  gitea_app_id = "my-gitea-oauth-app-id" # secret récupéré via vault dans la CI
}

app_roles

Ce module Terraform configure les rôles Vault nécessaires pour quune application déployée dans Kubernetes puisse :

  • sauthentifier auprès de Vault via son ServiceAccount,
  • obtenir des identifiants Postgres dynamiques,
  • accéder à ses secrets dans Vault.

🚀 Usage

module "webapp_vault_app" {
  source = "./modules/vault_app"
  name   = "webapp"
  database = "mydb" # optionnel, par défaut = name
}

principe

                          +-----------------+
                          |   Dépôt Git     |
                          | (CI/CD Terraform|
                          +--------+--------+
                                   |
                          [Auth via Vault JWT Role]
                                   |
                        +----------v-----------+
                        | Vault (Policy -ops) |
                        |   - Peut gérer      |
                        |     - Roles K8s     |
                        |     - Roles Postgres|
                        |     - Secrets KV    |
                        +----------+-----------+
                                   |
                        [Token éphémère CI/CD]
                                   |
                  +----------------v----------------+
                  | Kubernetes API                  |
                  | - Applique CRDs / Secrets       |
                  | - Configure Longhorn / RBAC     |
                  +---------------------------------+


   --------------------------- Flux runtime ---------------------------

                          +-----------------+
                          |     Pod App     |
                          | (SA: webapp)    |
                          +--------+--------+
                                   |
                        [Auth via Vault K8s Role]
                                   |
                        +----------v-----------+
                        | Vault (Policy app)  |
                        |   - Peut lire       |
                        |     - kvv2/data/... |
                        |     - postgres/...  |
                        +---------------------+
                                   |
                        [Secrets dynamiques: PW DB, etc.]
                                   |
                          +--------v---------+
                          |   Postgres DB    |
                          +------------------+

documentation destinée aux dépots des applications:

🔑 Gestion des secrets avec Vault Secrets Operator (VSO)

Ce repository utilise Vault Secrets Operator pour gérer les secrets de lapplication (notamment les identifiants Postgres).
Lobjectif est d’éviter de stocker des credentials statiques, en déléguant la génération et la rotation à HashiCorp Vault.


⚙️ Architecture

  1. Terraform côté admin configure Vault :

    • un backend Postgres (postgres/) connecté à la base via pgbouncer,
    • un rôle Vault webapp (postgres/roles/webapp) qui définit la manière dont les credentials dynamiques sont créés,
    • un rôle Kubernetes webapp (auth/kubernetes/role/webapp) qui autorise le ServiceAccount webapp du namespace à sauthentifier auprès de Vault.
  2. Lapplication (dans ce repo) déclare :

    • un VaultAuth qui associe le ServiceAccount webapp au rôle Vault webapp,
    • un VaultDynamicSecret qui demande un secret dynamique (postgres/creds/webapp),
    • un Secret Kubernetes généré automatiquement par VSO (vso-db-credentials), injecté dans le Pod de lapplication.

🛠️ Ressources déployées

VaultConnection

apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
  finalizers:
  - vaultconnection.secrets.hashicorp.com/finalizer
  labels:
  name: default
  namespace: {{ .Release.Namespace }}
spec:
  address: http://hashicorp-vault.tools.svc.cluster.local:8200
  skipTLSVerify: false

VaultAuth

apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
  name: auth
  namespace: {{ .Release.Namespace }}
spec:
  vaultConnectionRef: default
  method: kubernetes
  mount: kubernetes
  kubernetes:
    role: webapp
    serviceAccount: {{ include "webapp.serviceAccountName" . }}
    audiences:
      - vault

Permet à VSO (et donc à lapp) de sauthentifier auprès de Vault avec le rôle webapp.

VaultDynamicSecret

apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
  name: vso-db
  namespace: {{ .Release.Namespace }}
spec:
  mount: postgres
  path: creds/webapp   # chemin du rôle dynamique Postgres
  destination:
    create: true
    name: vso-db-credentials
  rolloutRestartTargets:
  - kind: Deployment
    name: {{ include "webapp.fullname" . }}
  vaultAuthRef: auth

Demande un secret dynamique Postgres depuis Vault et le stocke dans un Secret Kubernetes nommé vso-db-credentials. Le Deployment de lapp est redémarré automatiquement à chaque rotation de credentials.

📦 Consommation du secret Une fois VSO en place, les credentials Postgres sont disponibles dans le Secret Kubernetes :

apiVersion: v1
kind: Pod
metadata:
  name: example
spec:
  containers:
  - name: app
    image: myapp:latest
    env:
      - name: DB_USERNAME
        valueFrom:
          secretKeyRef:
            name: vso-db-credentials
            key: username
      - name: DB_PASSWORD
        valueFrom:
          secretKeyRef:
            name: vso-db-credentials
            key: password

🔄 Rotation Vault génère des identifiants éphémères (par défaut TTL = 1h).

VSO renouvelle ou régénère automatiquement ces credentials.

Lorsquun nouveau secret est émis, le Deployment ciblé est redémarré pour recharger les variables denvironnement.

Résumé Pas de secrets stockés en clair dans Git ou Helm.

Rotation automatique des credentials Postgres.

Intégration fluide avec Kubernetes via ServiceAccounts.