Skip to content
Quickstart

Brume vs pganonymize for PostgreSQL anonymization

pganonymize and Brume are both command-line tools driven by a YAML file, both aimed at taking personal data out of PostgreSQL databases. Under the surface they work very differently, and each fits different situations — this page tries to say which, fairly.

pganonymizeBrume
FormPython package (pip install pganonymize), compatible with Python 2.7Native CLI from apt, dnf or Homebrew
How it worksConnects to a database and rewrites its tables in placeCopies a source into a separate target, transforming on the fly; the source is never modified
OutputThe anonymized database itself, optionally dumped with --dump-fileAnother PostgreSQL database, or a .sql file
RulesYAML schema: tables, fields, provider per fieldYAML brume.yml: tables, columns, strategy + semantic type
Transformationsfake.* (any Faker provider), choice, clear, mask, partial_mask, md5, set, uuid4, update_jsonFAKE, MASK, HASH, NULLIFY, FPE_ID, FPE_UUID, DATE_SHIFT, KEEP
Determinismmd5 and set are deterministic; fake.*, choice and uuid4 produce random valuesEvery strategy is keyed and deterministic, stable across releases of a major version
Keys and foreign keysNot described in the documentationRules on a key are applied to every column linked to it by a foreign key; PKs encrypted without collisions
SubsettingNo — search and excludes choose which rows get anonymized; truncate empties whole tablesFilters + foreign-key radius (fk_depth), parents kept so no FK dangles
Pre-flight checks--dry-run (changes are not committed)scan, plan, dry-run, audit --anonymity
LicenseMITApache 2.0

pganonymize rewrites the database it connects to. For each table it reads the rows, computes new values, and writes them back with UPDATE. Because of that, you run it on a copy — typically a restored backup in a non-production environment — never on production itself. The usual flow:

prod backup ──▶ restore into scratch DB ──▶ pganonymize (in place) ──▶ --dump-file

Brume reads the source and writes elsewhere. It connects to production with a read-only account, transforms each row as it streams, and writes to a target database or a .sql file:

prod (read-only) ──▶ brume (transform on the fly) ──▶ test DB or .sql

The practical difference: with pganonymize, an unmasked copy of the data exists somewhere until the anonymization finishes — you need to secure and clean it up. With Brume, real values never land outside the source. On the other hand, pganonymize needs no second database: if you already have a restored copy and just want to scrub it, it does that directly.

pganonymize’s fake.* providers use the Faker library, which generates random data for each row: run it twice and you get different names. choice and uuid4 are random too. md5 is deterministic — it’s a plain MD5 of the original value, with no secret key, so anyone holding a candidate value can recompute it.

Brume’s strategies are all deterministic and keyed by two secrets. HASH is an HMAC-SHA256, FAKE values are seeded by an HMAC of the original, FPE_ID / FPE_UUID use format-preserving encryption. Same input + same secrets = same output, every run. That keeps test fixtures stable and bug reports reproducible (“user 12227206 still fails after the refresh”) — and without the secrets, the output can’t be recomputed from a guess.

pganonymize’s documentation doesn’t describe how keys are handled: it identifies rows by their primary_key (default id) to write them back, and suggests listing related tables together under truncate so they can be emptied without a constraint error.

Brume treats keys as first-class. FPE_ID encrypts integer ids into integers, FPE_UUID UUIDs into UUIDs, both without collisions — and Brume applies the same transformation to every foreign key that references them. Constraints hold on the target without disabling triggers.

pganonymize works on the whole database it’s pointed at. Its search option takes a SQL condition that selects which rows to anonymize — rows that don’t match are left as they are — and excludes skips rows matching a pattern. truncate empties whole tables you don’t need.

Brume selects which rows to copy: seed rows from SQL filters, then every row within fk_depth foreign-key hops, plus the parents they need. The result is a small, self-contained database — see Subsetting without breaking foreign keys.

  • You already have a restored copy and want to scrub it in place, without a second database.
  • Your team lives in Python and wants pip install — including on legacy Python 2.7 platforms.
  • You want the breadth of Faker’s providers (fake.*) for fields Brume’s semantic types don’t cover.
  • You need choice, set or update_json (replace JSON values by data type) semantics, or to truncate whole tables.
  • Random, non-reproducible values are fine — or even preferred — for your use case.
  • You don’t want an unmasked copy of production to exist outside production, even temporarily.
  • You need deterministic, keyed output: stable fixtures, ids that survive refreshes, joins across databases pseudonymized separately.
  • Your primary keys need pseudonymizing too, with every foreign key following.
  • You need a subset, not a scrubbed full copy.
  • You want guardrails before writing: plan lists PII-looking columns no rule covers and refuses configurations that can only fail; audit --anonymity measures residual re-identification risk.