# Réplica Anonymisé

> Transcription française complète du chapitre « Réplica Anonymisé » du manuel DEV42, révision 26.09, pages PDF 547 à 566.

---

Index LLMS : [llms.txt](/fr/llms.txt)

---

<!-- Source: https://public.dalibo.com/exports/formation/manuels/formations/dev42/dev42.handout.html, révision 26.09. -->

# Réplica Anonymisé {#dev42-ch16}

## Anonymisation et Réplication Logique {#dev42-ch16-1}

- Réplica toujours à jour, toujours anonymisé
- Utilise la réplication logique de PostgreSQL
- Nouveauté depuis la version 3.0.0

## L’histoire {#dev42-ch16-2}

- Pierre a besoin d’un accès permanent aux données de la boutique
- Paul refuse un accès direct à la production
- Solution : un réplica dédié, anonymisé

## Comment ça marche ? {#dev42-ch16-3}

![Illustration du manuel DEV42](/images/dev42/html/ch16-466c7aa059c7.png)

## Objectifs {#dev42-ch16-4}

- Configurer la publication côté source
- Configurer le réplica anonymisé
- Vérifier l’anonymisation en temps réel

## Prérequis : réplication logique {#dev42-ch16-5}

``` ini
wal_level = logical
```

Puis redémarrer l’instance.

La réplication logique nécessite `wal_level = logical`. Sur une instance fraîche ce paramètre est à `replica` ; il faut le monter d’un cran.

Un redémarrage de l’instance est nécessaire :

``` bash
sudo systemctl restart postgresql-18
```

Vérifier :

``` sql
SHOW wal_level;
```

## Côté publication : rôle de réplication {#dev42-ch16-6}

``` sql
\c boutique paul

CREATE ROLE anon_replicator LOGIN REPLICATION;

GRANT USAGE  ON SCHEMA public TO anon_replicator;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon_replicator;
```

Définir un mot de passe pour ce rôle :

``` sql
\password anon_replicator
```

En tant qu’utilisateur système **postgres**, compléter le fichier **.pgpass** :

``` bash
echo "*:*:boutique:anon_replicator:<mot_de_passe>" >> ~/.pgpass
```

Mettre à jour le fichier `pg_hba.conf` de l’instance source pour autoriser la connexion en réplication depuis l’instance réplica :

``` ini
# pg_hba.conf de l'instance source
host  boutique  anon_replicator  127.0.0.1/32  scram-sha-256
host  boutique  anon_replicator  ::1/128       scram-sha-256
```

``` sql
SELECT pg_reload_conf();
```

## Côté publication : créer la publication {#dev42-ch16-7}

``` sql
\c boutique paul

CREATE PUBLICATION pub_boutique
  FOR TABLE company, supplier;
```

On publie uniquement les tables que Pierre doit pouvoir consulter. Les tables `customer` et `payout` ne sont pas publiées ici.

Vérifier la publication :

``` sql
SELECT * FROM pg_publication;
SELECT * FROM pg_publication_tables;
```

## Côté réplica : nouvelle instance {#dev42-ch16-8}

Créer une seconde instance sur le port **5433** :

``` bash
# RHEL
sudo -u postgres /usr/pgsql-18/bin/initdb -D /var/lib/pgsql/18/replica
sudo sed -i "s/^#port = 5432/port = 5433/" \
  /var/lib/pgsql/18/replica/postgresql.conf
sudo -u postgres /usr/pgsql-18/bin/pg_ctl start \
  -D /var/lib/pgsql/18/replica
```

## Côté réplica : créer la base {#dev42-ch16-9}

``` bash
createuser -p 5433 -s paul
createuser -p 5433 pierre
createdb -p 5433 -O paul boutique_replica
```

Charger l’extension `anon` via `shared_preload_libraries` :

``` bash
echo "shared_preload_libraries = 'anon'" \
  >> /var/lib/pgsql/18/replica/postgresql.conf
sudo -u postgres /usr/pgsql-18/bin/pg_ctl restart \
  -D /var/lib/pgsql/18/replica
```

L’extension doit passer par `shared_preload_libraries` (et non `session_preload_libraries`) car les workers de réplication logique ne chargent pas ces derniers : sans ce réglage, les règles de masquage ne s’appliqueraient pas aux données entrantes.

## Côté réplica : activer le masquage {#dev42-ch16-10}

``` sql
-- connexion à l'instance réplica
\c "port=5433 dbname=boutique_replica user=paul"

CREATE EXTENSION IF NOT EXISTS anon;
SELECT anon.init();

ALTER DATABASE boutique_replica
  SET anon.replica_masking TO on;
```

Se reconnecter, puis accorder les droits de lecture à Pierre :

``` sql
\c "port=5433 dbname=boutique_replica user=paul"
GRANT USAGE ON SCHEMA public TO pierre;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO pierre;
```

Le paramètre `anon.replica_masking` doit être activé *avant* la création de la souscription. Il indique à l’extension d’intercepter les données entrantes pour y appliquer les règles de masquage. La reconnexion est nécessaire pour que la session prenne en compte la nouvelle valeur du paramètre.

Pierre dispose ainsi d’un accès en lecture seule aux données anonymisées, sans jamais accéder à la base de production.

## Côté réplica : créer les tables {#dev42-ch16-11}

Les tables doivent exister **avant** la souscription :

``` sql
CREATE TABLE company (
    id SERIAL PRIMARY KEY,
    name TEXT,
    vat_id TEXT UNIQUE
);

CREATE TABLE supplier (
    id SERIAL PRIMARY KEY,
    fk_company_id INT REFERENCES company(id),
    contact TEXT,
    phone TEXT,
    job_title TEXT
);
```

## Côté réplica : règles de masquage {#dev42-ch16-12}

Les mêmes règles que pour le masquage dynamique :

``` sql
SECURITY LABEL FOR anon ON COLUMN company.name
  IS 'MASKED WITH FUNCTION anon.pseudo_company(id)';

SECURITY LABEL FOR anon ON COLUMN company.vat_id
  IS 'MASKED WITH FUNCTION anon.random_string(10)';

SECURITY LABEL FOR anon ON COLUMN supplier.contact
  IS 'MASKED WITH VALUE $$CONFIDENTIAL$$';
```

Les règles de masquage sont déclarées **sur le réplica**, pas sur la source. C’est le réplica qui applique l’anonymisation au moment de la réception des données.

La pseudonymisation de `company.name` garantit que Pierre verra toujours le même nom fictif pour une même société, ce qui lui permet de faire des jointures cohérentes entre `company` et `supplier`.

``` sql
SECURITY LABEL FOR anon ON COLUMN supplier.phone
  IS 'MASKED WITH FUNCTION anon.partial(phone,0,$$XX-XXX-XXX$$,0)';
```

## Côté réplica : activer et souscrire {#dev42-ch16-13}

``` sql
\c "port=5433 dbname=boutique_replica user=paul"

SELECT anon.start_replica_masking();

CREATE SUBSCRIPTION sub_boutique
  CONNECTION 'host=localhost port=5432
              user=anon_replicator
              password=CHANGEME
              dbname=boutique'
  PUBLICATION pub_boutique;
```

`anon.start_replica_masking()` installe le trigger logique qui intercepte les données entrantes.

Le mot de passe dans la chaîne de connexion peut être remplacé par une entrée dans le fichier `.pgpass` de l’utilisateur système qui exécute l’instance réplica.

Vérifier l’état de la souscription :

``` sql
SELECT * FROM pg_stat_subscription;
```

``` default
 subname       | pid  | received_lsn | last_msg_receipt_time | latest_end_lsn
---------------+------+--------------+-----------------------+----------------
 sub_boutique  | 1234 | 0/1A2B3C4D   | 2025-03-24 ...        | 0/1A2B3C4D
```

## Vérification {#dev42-ch16-14}

Sur le réplica, les données sont anonymisées, y compris pour Paul :

``` sql
\c "port=5433 dbname=boutique_replica user=paul"
SELECT * FROM company;
```

``` default
 id  |      name       |   vat_id
-----+-----------------+------------
 952 | Wilkinson LLC   | IKL88GJVT4
 194 | Johnson PLC     | VOOJ6UKR6H
 346 | Young-Carpenter | DUR78F15VD
```

Contrairement au masquage dynamique, **tous les utilisateurs** voient les données anonymisées sur le réplica, car les données originales n’y sont jamais stockées.

``` sql
\c boutique paul
SELECT * FROM company;
```

``` default
 id  |        name         |     vat_id
-----+---------------------+----------------
 952 | Shadrach            | FR62684255667
 194 | Johnny's Shoe Store | CHE670945644
 346 | Capitol Records     | GB663829617823
```

## Attention aux clés primaires {#dev42-ch16-15}

- La réplication logique identifie les lignes par leur clé primaire
- **Ne pas masquer les colonnes de la clé primaire !**
- Sinon les `UPDATE` et `DELETE` ne se propagent plus

Les clés primaires techniques (séquences, UUID) ne sont généralement pas des données personnelles et n’ont pas besoin d’être masquées.

## Exercices {#dev42-ch16-16}

Pour rappel, sauf précision contraire, toutes les commandes sont à exécuter en tant que **paul** (le propriétaire).

### E501 - Créer une nouvelle instance {#dev42-ch16-16-1}

- Vérifier que `wal_level = logical` est actif sur l’instance source.
- Créer une seconde instance PostgreSQL sur le port 5433.
- Créer la base `boutique_replica` sur cette instance.
- Installer et initialiser l’extension `anon` dans `boutique_replica`.

### E502 - Configurer le réplica anonymisé {#dev42-ch16-16-2}

- Sur l’instance source, créer le rôle `anon_replicator` et la publication pour les tables `company` et `supplier`.
- Sur le réplica, recréer le schéma des tables `company` et `supplier`.
- Déclarer les règles de masquage sur le réplica (reprendre celles du masquage dynamique).
- Activer le replica masking, lancer `anon.start_replica_masking()` et créer la souscription.

### E503 - Vérifier la réplication anonymisée {#dev42-ch16-16-3}

- Sur le réplica, vérifier que les données de `company` et `supplier` sont anonymisées.
- Sur la source, insérer une nouvelle société et un nouveau fournisseur.
- Sur le réplica, vérifier que les nouvelles données apparaissent et sont anonymisées.

## Solutions {#dev42-ch16-17}

### S501 {#dev42-ch16-17-1}

``` sql
-- Sur l'instance source
SHOW wal_level;
```

``` default
 wal_level
-----------
 logical
```

Si la valeur est `replica`, modifier `postgresql.conf` et redémarrer.

Créer l’instance réplica :

``` bash
sudo -u postgres /usr/pgsql-18/bin/initdb -D /var/lib/pgsql/18/replica
sudo sed -i "s/^#port = 5432/port = 5433/" \
  /var/lib/pgsql/18/replica/postgresql.conf
sudo -u postgres /usr/pgsql-18/bin/pg_ctl start \
  -D /var/lib/pgsql/18/replica
```

Créer les rôles et la base :

``` bash
createuser -p 5433 -s paul
createuser -p 5433 pierre
createdb -p 5433 -O paul boutique_replica
```

Installer l’extension (via `shared_preload_libraries`, nécessaire pour les workers de réplication logique) :

``` bash
echo "shared_preload_libraries = 'anon'" \
  >> /var/lib/pgsql/18/replica/postgresql.conf
pg_ctl restart -D /var/lib/pgsql/18/replica

psql -p 5433 -d boutique_replica <<EOF
CREATE EXTENSION IF NOT EXISTS anon;
SELECT anon.init();
EOF
```

### S502 {#dev42-ch16-17-2}

Sur la source (port 5432) :

``` sql
\c boutique paul

CREATE ROLE anon_replicator LOGIN REPLICATION;
\password anon_replicator

GRANT USAGE ON SCHEMA public TO anon_replicator;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon_replicator;

CREATE PUBLICATION pub_boutique FOR TABLE company, supplier;
```

Sur le réplica (port 5433) :

``` sql
\c "port=5433 dbname=boutique_replica user=paul"

CREATE TABLE company (
    id SERIAL PRIMARY KEY,
    name TEXT,
    vat_id TEXT UNIQUE
);

CREATE TABLE supplier (
    id SERIAL PRIMARY KEY,
    fk_company_id INT REFERENCES company(id),
    contact TEXT,
    phone TEXT,
    job_title TEXT
);

SECURITY LABEL FOR anon ON COLUMN company.name
  IS 'MASKED WITH FUNCTION anon.pseudo_company(id)';

SECURITY LABEL FOR anon ON COLUMN company.vat_id
  IS 'MASKED WITH FUNCTION anon.random_string(10)';

SECURITY LABEL FOR anon ON COLUMN supplier.contact
  IS 'MASKED WITH VALUE $$CONFIDENTIAL$$';

ALTER DATABASE boutique_replica
  SET anon.replica_masking TO on;

GRANT USAGE ON SCHEMA public TO pierre;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO pierre;
```

Se reconnecter puis activer et souscrire :

``` sql
\c "port=5433 dbname=boutique_replica user=paul"

SELECT anon.start_replica_masking();

CREATE SUBSCRIPTION sub_boutique
  CONNECTION 'host=localhost port=5432
              user=anon_replicator
              password=CHANGEME
              dbname=boutique'
  PUBLICATION pub_boutique;
```

### S503 {#dev42-ch16-17-3}

Vérifier les données anonymisées sur le réplica :

``` sql
\c "port=5433 dbname=boutique_replica user=paul"

SELECT * FROM company;
```

``` default
 id  |      name       |   vat_id
-----+-----------------+------------
 952 | Wilkinson LLC   | IKL88GJVT4
 194 | Johnson PLC     | VOOJ6UKR6H
 346 | Young-Carpenter | DUR78F15VD
```

``` sql
SELECT s.contact, s.job_title, c.name
FROM supplier s
JOIN company c ON s.fk_company_id = c.id;
```

``` default
   contact    |   job_title   |      name
--------------+---------------+-----------------
 CONFIDENTIAL | CEO           | Johnson PLC
 CONFIDENTIAL | Sales manager | Young-Carpenter
```

Ajouter des données sur la source :

``` sql
\c boutique paul

INSERT INTO company VALUES (501, 'Acme Corp', 'US123456789');
INSERT INTO supplier VALUES (42, 501, 'Wile E. Coyote', '555-0100', 'Buyer');
```

Vérifier la propagation sur le réplica :

``` sql
\c "port=5433 dbname=boutique_replica user=paul"

SELECT s.contact, s.job_title, c.name
FROM supplier s
JOIN company c ON s.fk_company_id = c.id
WHERE s.id = 42;
```

``` default
   contact    | job_title |     name
--------------+-----------+--------------
 CONFIDENTIAL | Buyer     | Martinez Ltd
```

La donnée a été répliquée et anonymisée automatiquement.
## Navigation

Pour revenir au chapitre précédent, voir [le chapitre précédent](/fr/dev42/15-vues-de-masquage/#dev42-ch15).
Pour poursuivre la lecture, voir [le chapitre suivant](/fr/dev42/17-conclusion-postgresql-anonymizer/#dev42-ch17).
