Aller au contenu
Démarrage rapide

Comment anonymiser une base PostgreSQL pour les tests

Des tests sur des données réalistes trouvent les bugs que les fixtures écrites à la main ratent : encodages exotiques, volumes déséquilibrés, cas limites auxquels personne n’avait pensé. Mais une copie brute de la production place les données personnelles de tous vos clients dans un environnement plus ouvert et moins protégé.

Ce tutoriel construit une base de test à partir de la production avec Brume : un sous-ensemble des lignes, des données personnelles remplacées à la volée, des clés primaires chiffrées, et toutes les clés étrangères toujours valides.

Un schéma e-commerce classique :

users ◀── orders ◀── order_items ──▶ products

L’objectif : une base de test contenant les commandes passées depuis le 1er janvier 2025, les utilisateurs qui les ont passées, leurs lignes de commande et les produits référencés — noms, emails, téléphones, adresses et texte libre pseudonymisés, et users.id / orders.id chiffrés de façon cohérente partout où ils apparaissent.

  • Brume installé — voir Démarrage (Debian/Ubuntu, Fedora/CentOS, macOS).
  • pg_dump dans le PATH, en version majeure au moins égale à celle de la source. Brume l’utilise pour copier le schéma.
  • Une cible : une base PostgreSQL vide, ou un répertoire accessible en écriture si vous préférez un fichier .sql (variante plus bas).

Étape 1 — Créer un compte en lecture seule sur la source

Section intitulée « Étape 1 — Créer un compte en lecture seule sur la source »

Brume n’écrit jamais sur la source. Un compte en lecture seule garantit qu’une faute de frappe n’y changera rien :

CREATE ROLE brume_reader WITH LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE shop TO brume_reader;
GRANT USAGE ON SCHEMA public TO brume_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO brume_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO brume_reader;

Étape 2 — Configurer les connexions et les secrets

Section intitulée « Étape 2 — Configurer les connexions et les secrets »

Générez un .env.brume pour une cible PostgreSQL :

Fenêtre de terminal
brume init jdbc

Générez deux secrets à forte entropie. BRUME_HMAC_SECRET demande au moins 16 caractères (32+ recommandés) ; BRUME_FPE_KEY doit faire exactement 16, 24 ou 32 caractères :

Fenêtre de terminal
openssl rand -base64 48 # → BRUME_HMAC_SECRET (64 caractères)
openssl rand -hex 16 # → BRUME_FPE_KEY (32 caractères)

Puis remplissez .env.brume :

Fenêtre de terminal
# Secrets — les « informations supplémentaires » de l'art. 4(5) du RGPD. Jamais dans git.
BRUME_HMAC_SECRET=<sortie de openssl rand -base64 48>
BRUME_FPE_KEY=<sortie de openssl rand -hex 16>
# Source (lecture seule)
BRUME_SOURCE_HOST=db.prod.internal
BRUME_SOURCE_DB=shop
BRUME_SOURCE_USER=brume_reader
BRUME_SOURCE_PASSWORD=...
BRUME_SOURCE_SSLMODE=require
BRUME_SCHEMA=public
# Cible — une base de test vide
BRUME_SINK_TYPE=JDBC
BRUME_TARGET_HOST=localhost
BRUME_TARGET_PORT=5432
BRUME_TARGET_DB=shop_test
BRUME_TARGET_USER=app
BRUME_TARGET_PASSWORD=...

Validez la configuration — longueur des secrets, clé FPE, réglages du sink :

Fenêtre de terminal
brume diag

diag ne se connecte à rien. La connectivité et la version de pg_dump sont vérifiées par plan et execute avant de lire la moindre ligne — voir Contrôles préalables.

Chaque variable est documentée dans la référence .env.brume.

Fenêtre de terminal
brume scan

scan lit uniquement les noms et les types des colonnes — jamais vos données — et écrit un brume.yml pré-rempli : les colonnes qui ressemblent à des données personnelles reçoivent une règle (email → FAKE EMAIL, phone → MASK PHONE, password → HASH…), les clés primaires reçoivent FPE_ID ou FPE_UUID, les clés étrangères sont volontairement omises car elles héritent de la règle de la clé qu’elles référencent.

C’est une proposition fondée sur les noms : relisez chaque ligne. Le bloc extraction est laissé en TODO — choisir les lignes à copier est une décision métier.

Après relecture, le fichier ressemble à ceci :

extraction:
fk_depth: 1 # rayon : les commandes 2025, leurs users, leurs lignes
tables:
- table: orders
filter: "created_at >= '2025-01-01'"
anonymization:
tables:
- table: users
columns:
- name: id
strategy: FPE_ID # appliqué aussi à orders.user_id, automatiquement
- name: email
strategy: FAKE
type: EMAIL
- name: first_name
strategy: FAKE
type: FIRST_NAME
- name: last_name
strategy: FAKE
type: LAST_NAME
- name: phone
strategy: MASK
type: PHONE
- name: birth_date
strategy: DATE_SHIFT # ± jusqu'à 30 jours, garde les tranches d'âge
- name: password_hash
strategy: HASH
- name: notes
strategy: NULLIFY # texte libre : on le supprime (colonne nullable)
- table: orders
columns:
- name: id
strategy: FPE_ID # appliqué aussi à order_items.order_id
- name: shipping_address
strategy: FAKE
type: ADDRESS
- name: metadata
strategy: FAKE
type: JSONB
json_paths:
- path: $.contact.email
type: EMAIL
- path: $.contact.phone
type: PHONE

Pourquoi ces choix :

ColonneRègleRaison
users.id, orders.idFPE_IDChiffrement préservant le format : entier en entrée, entier en sortie, sans collision. Brume trouve chaque clé étrangère qui pointe vers la colonne et la réécrit avec le même chiffrement.
email, noms, shipping_addressFAKE + typeDes valeurs réalistes : le code de validation et les captures d’écran fonctionnent toujours. Déterministe : la même valeur réelle donne toujours le même faux.
phoneMASKGarde la forme (préfixe, derniers chiffres) pour le debug façon support. Refusé sur une colonne UNIQUE — utilisez FAKE dans ce cas.
birth_dateDATE_SHIFTDécale chaque date d’un nombre de jours déterministe : les distributions d’âge restent exploitables.
password_hashHASHHMAC-SHA256 à clé : opaque, stable, jamais affiché.
notesNULLIFYUn texte libre peut contenir n’importe quoi. Le supprimer est la minimisation la plus forte.
metadataFAKE + JSONBSeuls les chemins listés sont réécrits ; le reste du document est conservé.
products (non déclarée)KEEPPas de données personnelles. Les colonnes non déclarées sont copiées telles quelles.

products est copiée alors qu’elle n’est pas dans extraction.tables : avec la valeur par défaut orphan_policy: KEEP_PARENTS, chaque parent référencé par une ligne copiée est copié aussi, pour qu’aucune clé étrangère ne pointe dans le vide. Les détails sont dans la référence brume.yml et dans Stratégies de pseudonymisation.

Étape 5 — Plan : ce qui sera copié, ce qui n’est pas couvert

Section intitulée « Étape 5 — Plan : ce qui sera copié, ce qui n’est pas couvert »
Fenêtre de terminal
brume plan

plan est en lecture seule. Il compte les lignes que chaque table recevra — exactement ce que copiera execute — et liste les colonnes qui ressemblent à des données personnelles sans aucune règle. Toute colonne de cette liste sera copiée telle quelle : ajoutez une règle, ou un strategy: KEEP explicite pour documenter qu’elle est sans risque.

plan refuse aussi les configurations vouées à l’échec — NULLIFY sur une colonne NOT NULL, MASK sur une colonne UNIQUE — avec le code de sortie 1, avant toute écriture.

Fenêtre de terminal
brume dry-run

Extraction, transformation et écriture s’exécutent, mais l’écriture ne va nulle part. Cela détecte ce que plan ne voit pas : chemins JSONB qui ne résolvent jamais, incohérences de types, échecs de résolution des clés étrangères.

Fenêtre de terminal
brume execute

Brume copie le schéma avec pg_dump, lit en flux les lignes sélectionnées sur la source, transforme chaque colonne et écrit dans shop_test. Il termine par un résumé par table et le temps total d’exécution.

Les lignes annoncées par plan : les commandes 2025, leurs utilisateurs, leurs lignes de commande, les produits que ces lignes référencent. Rien d’autre.

Les données personnelles remplacées. Illustration — les faux exacts dépendent de vos secrets et de la locale :

ColonneSourceCible
users.id112227206
orders.user_id112227206 — même valeur, la jointure fonctionne
users.emailjane.doe@acme.ioune fausse adresse comme alice.smith@example.com
users.phone+33 6 12 34 56 78+33 6 ** ** ** 78
users.birth_date1984-03-12une date à ±30 jours
users.notestexte libreNULL

La sortie de FPE_ID est un entier, mais pas de la même longueur : les petits identifiants ressortent plus longs. Jusqu’à 2 147 483 647, ils restent dans la plage d’un integer ; au-delà, il faut un bigint.

Les clés étrangères valides. Les contraintes n’ont jamais été désactivées. Vérifiez-le vous-même sur la cible :

-- Attendu : 0 — chaque commande pointe vers un utilisateur existant
SELECT count(*)
FROM orders o
LEFT JOIN users u ON u.id = o.user_id
WHERE u.id IS NULL;

Reproductible. Relancez brume execute avec les mêmes secrets : vous obtenez les mêmes valeurs. Un bug reproduit aujourd’hui sur users.id = 12227206 est toujours sur 12227206 après le prochain rafraîchissement. Cette stabilité vaut pour toutes les versions d’une même version majeure.

Les collisions signalées, pas cachées. Si deux lignes obtiennent la même valeur pseudonymisée sur une colonne UNIQUE, la seconde n’est pas écrite et la perte est signalée par table ([UNIQUE_COLLISION] dans les logs). Pour zéro collision sur une colonne unique, utilisez HASH, FPE_ID ou FPE_UUID — voir Colonnes UNIQUE et collisions.

Pas de base cible sous la main, ou un fichier à remettre à un relecteur ? Générez le sink dump :

Fenêtre de terminal
brume init dump
Fenêtre de terminal
BRUME_SINK_TYPE=DUMP
BRUME_SINK_OUTPUT_PATH=./out/shop_test.sql.gz
BRUME_SINK_COMPRESSION=GZIP # NONE | GZIP | ZSTD

brume execute écrit alors le schéma et les lignes pseudonymisées dans le fichier. Restaurez-le où vous voulez :

Fenêtre de terminal
gunzip -c ./out/shop_test.sql.gz | psql -h localhost -U app -d shop_test
  • Traitez-la comme une donnée personnelle. Limitez qui y a accès et fixez une durée de conservation. La page RGPD & conformité contient une checklist DPO.
  • Protégez les secrets comme les données source elles-mêmes : qui détient BRUME_HMAC_SECRET et BRUME_FPE_KEY détient le lien vers les personnes réelles.
  • Mesurez le risque résiduel. Des combinaisons comme date de naissance + code postal + sexe peuvent isoler une personne même sans nom. brume audit --anonymity le mesure.