11°
Portada del artículo: pg_anon found my database's emails, but not the RUT
PostgreSQLPersonal dataAnonymizationpg_anonDatabases

pg_anon found my database's emails, but not the RUT

I tried pg_anon, the Russian tool that masks personal data in Postgres. With stock rules it caught 1 of 8 PII columns on a Chilean database; with my own rules, all 8. I measured recall, integrity and speed against pg_dump.

Efrain Garay 5 September 2026

In 47 seconds: pg_anon caught 1 of 8 columns holding personal data on a Chilean database, and all 8 once I wrote the rules. The RUT is blind by default.Watch it in the reel viewer →

I want to hand a developer a copy of the production database. The problem is obvious: that database holds names, emails, phone numbers and RUT (the Chilean national ID) of real people. Copying it as-is to a test environment means leaking personal data, and the new Chilean law that punishes that takes effect in December 2026. What I need is a pg_dump that also masks the sensitive parts on the way out.

That is what pg_anon promises, an open tool from TantorLabs that surfaced on Habr and that, as far as I searched, nobody has tested in Spanish. I installed it, fed it a Chilean toy database and measured three things: how much of the personal information it finds on its own, whether the masked copy is still a coherent database, and what it costs against a plain pg_dump.

What it is, and how it differs from pg_dump

pg_anon does not replace pg_dump: it wraps it. The flow has four steps. First init creates an anon_funcs schema in the source database with the functions that later replace values. Then create-dict scans the database and builds a dictionary of which columns are sensitive. dump exports the data applying that dictionary, and that is where masking happens. Finally restore loads the clean copy into an empty database.

The piece that decides everything is the scan. And the scan runs on rules you give it, in a two-filter cascade: first it reads each column’s name against some regular expressions; to the ones left over, it opens the data and tries other expressions on the value. Whatever no filter catches passes through. That is the detail the reviews I read skip over.

The scan, filter by filterthe real order in create_dict.py
8 columns with PII
nombreapellidoemailtelefonorutdireccionfecha_nactarjeta_ult4
1
by namereads the column's label against field.rules
nombreapellidoemailtelefonorutdireccionfecha_nactarjeta_ult4
2
by contentonly the leftovers: opens the data and tries data_regex
nombreapellidoemailtelefonorutdireccionfecha_nactarjeta_ult4
sensitive
passes through
stock rules English1 / 8only email, by its name and its @
Spanish rules + RUT8 / 8all 8 fall in filter 1, by name
Measured on a 500-row table. Only a hand-written rule catches the RUT.

The install, with its snags

I brought up a PostgreSQL 17 in Docker and a database with two tables: clientes (500 rows, with name, surname, email, phone, RUT, address, city, birth date and notes) and pedidos (2000 rows, with a foreign key to clientes and the last four digits of the card). Eight of those columns are personal data. Fake data but with the real shape of Chilean data: RUTs computed with their modulo 11, +569 mobiles, emails built from name and surname.

Installing pg_anon inside the container needed pip install --break-system-packages, because Debian blocks global pip since bookworm, and nothing more. Version 1.11.0. It needs pg_dump and pg_restore of the same major version as the server, which the container already ships.

The finding: it detects what it can name

I ran the scan twice over the same database. First with the stock meta-dict (the rule file that guides the scan), with its English patterns. Then with my own meta-dict: the same column names but in Spanish, plus a regular expression for the RUT.

What it flagged as personal data
stock rules: 1 / 8Spanish rules + RUT: 8 / 8
columnstock rulesSpanish rules + RUT
nombretext
apellidotext
emailtext
telefonotext
ruttext
direcciontext
fecha_nacdate
tarjeta_ult4char(4)
Recall over the 8 PII columns of the clientes table

With the stock rules, pg_anon flagged one of the eight columns holding personal data: email. It caught it because email is spelled the same in English and matches a stock name rule; its content, with the at-sign, confirms it twice over. The other seven (name, surname, phone, address, birth date, the card’s four digits and the RUT) passed through. The RUT is the clearest case: no stock rule recognizes a Chilean identifier, and its shape (seven or eight digits, a dash and a check digit) looks like nothing the default dictionary searches for.

With the adapted meta-dict, recall jumped to eight of eight, without a single false positive: it left the city, the notes and the non-personal columns untouched. The tool detects well; the work is teaching it the language and the country’s identifiers.

The copy is a coherent database

With the full dictionary, dump and restore rebuilt the database into an empty copy. I verified what matters in masking: that the sensitive parts are covered and the rest still works.

CheckResult
PII columns masked8 of 8
Rows preserved500 clients, 2000 orders (identical)
Orphan orders after restore0
Unique emails after the hash499 of 500

Client 1 went from Fernanda / fernanda.diaz4@outlook.com / 7917183-2 to three SHA-256 hashes. Referential integrity stayed perfect: zero orders pointing to a nonexistent client, because the foreign keys go by id, which is not touched. And the hash keeps uniqueness: the source had 499 distinct emails across the 500 rows, and after the hash they are still 499, so grouping by email counts the same.

What it costs

Over 2500 rows, masking is paid in time and size:

Operationpg_anonplain pg_dump
Export (dump)0.33 s0.064 s
Dump size168 KB72 KB

Five times slower and more than twice as heavy. It is expected: for every sensitive value it runs a SQL function, and a 64-character hash weighs more than the email or RUT it replaces. On a 2500-row database it does not show; on one with millions you would have to measure again before promising anything.

When I would use it, and when not

Yes, when I need a production copy for development or testing and the schema is stable: the work of writing the dictionary is done once and stays. The referential integrity it preserves is the strong point, because that is exactly what breaks when you try to anonymize by hand with UPDATE.

No, if I expect it to detect personal data on its own in a Spanish database. It does not: you have to write the rules, column by column, and verify recall against a known database before trusting the copy. And a warning the documentation itself makes and is worth repeating: this is pseudonymization, not anonymization in the GDPR sense. The hash makes it plain: unsalted, a RUT has only some 25 million possible values, so whoever holds the hash reverses it by enumeration in minutes, and the same goes for a phone or a date. It reduces exposure; it does not remove it.

pg_anon is a good tool you have to speak to in its language. For a Chilean database, that language includes a rule for the RUT that nobody will write for you. So I wrote it: the Spanish meta-dict, with the RUT rule, is in a public repository so you don’t have to start from scratch.

Sources

Comments

No comments yet. The first one is yours.

Reviewed before publishing. The email is not stored and never appears anywhere.