
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.
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.
create_dict.pynombreapellidoemailtelefonorutdireccionfecha_nactarjeta_ult4field.rulesdata_regexemail, by its name and its @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.
nombretext✗✓apellidotext✗✓emailtext✓✓telefonotext✗✓ruttext✗✓direcciontext✗✓fecha_nacdate✗✓tarjeta_ult4char(4)✗✓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.
| Check | Result |
|---|---|
| PII columns masked | 8 of 8 |
| Rows preserved | 500 clients, 2000 orders (identical) |
| Orphan orders after restore | 0 |
| Unique emails after the hash | 499 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:
| Operation | pg_anon | plain pg_dump |
|---|---|---|
| Export (dump) | 0.33 s | 0.064 s |
| Dump size | 168 KB | 72 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
- pg_anon, by TantorLabs (repository), version 1.11.0, tested against PostgreSQL 17.
- Scan documentation (create-dict) and dump.
- pg-anon-rules-es: the Spanish meta-dict with the RUT rule, ready to use.
Comments
No comments yet. The first one is yours.