4 min read

Masking Production Data Is Not Anonymisation

There is a script at most companies called something like scrub.sql. It runs after a production restore. It replaces names with fake names, emails with user123@example.com, and phone numbers with a pattern.

Once it has run, the environment is described as anonymised, and everyone stops thinking about it.

I want to argue that this is the most comfortable wrong belief in the whole test data area, and that the comfort is the dangerous part.

Identity is not in the name field

The intuition behind masking is that identity lives in identifiers. Remove the name, the email, the phone number, and you have removed the person.

That is not how re-identification works. People are identified by combinations of attributes that are individually unremarkable and jointly unique.

Take a masked customer record. The name is gone. What remains might be: signup date, city, plan tier, seat count, industry, renewal month, and a transaction history.

If you are the only enterprise account in Wellington on the annual plan with 340 seats, your name being Jane Doe protects nothing. Anyone with a rough sense of the customer base identifies that row immediately, and quite a lot of people have a rough sense of the customer base. Sales does. Support does. Anyone who has read a case study does.

The academic version of this has been known for decades. Small combinations of quasi identifiers, things like postcode, birth date, and gender, are enough to uniquely pick out most people in a population. The lesson generalises: the fields you kept because they were "not personal" are doing the identifying.

Coverage decays automatically

Set aside the theory. There is a mechanical problem too.

A masking script covers the columns someone enumerated when they wrote it. Schemas change every sprint. Scripts change when someone remembers.

So the gap grows on its own. A notes column where support pasted a full name and a phone number. A metadata JSON blob that accumulated keys nobody catalogued. An audit_log with a message template that interpolates the customer name. A new column added in March.

Nobody decided to leave those exposed. The script simply did not know about them, and nothing in the process forces it to find out. Masking coverage is a snapshot of one person's understanding of the schema on the day they wrote it.

Meanwhile the environment is labelled anonymised, so nobody looks.

Free text is unfixable

Even with perfect column coverage, free text defeats masking.

Support tickets, comments, descriptions, internal notes. These contain names, addresses, account numbers, and occasionally things far more sensitive, in unpredictable positions and formats.

You cannot regex your way out of this. You can run entity recognition over it, which will find most of it and miss some, and "most" is not a standard that means anything when the failure mode is one record.

The usual response is to blank those columns entirely. Which is reasonable, and which also means the environment no longer contains the data your search feature was supposed to be tested against. You have traded a privacy problem for a fidelity problem.

Where the line actually is

The distinction worth holding onto is this.

Masking transforms real records. There is a real person behind every row. Whether they can be picked out is a question about how much you removed and how much an attacker knows. The answer is usually "more than you would like".

Synthesis generates records that were never derived from a person. There is no one behind the row. The question of re-identification does not arise, because there is nothing to re-identify.

That is a difference in kind, not degree. It is also why the two get regulated differently: masked data is generally still personal data, with all the obligations that implies, and genuinely synthetic data is not.

I am not a lawyer and this is not legal advice. But the practical version is simple. If a row can be traced back to a human, you still own the risk, no matter what the script replaced.

The honest tradeoff

Synthesis is not free.

Masked production data is real, and real data has texture you will not think to generate. The genuinely odd records, the account created in 2011 that has been through three migrations, the malformed address that somehow passed validation in 2016. Those are useful and they are hard to invent.

Synthetic data has to earn its place by being structurally correct, appropriately skewed, and large enough to be representative. If it is uniform and thin, people will go back to the dump, and they will be right to.

So the argument is not that masking is useless. It is that masking is a risk reduction measure being sold internally as a risk elimination measure, and the label is doing damage. Call it what it is, and the conversation about what to do next gets easier.

Where SeedQL sits

SeedQL generates from your schema rather than transforming your records. No production row is read, so there is no masking coverage to maintain, no free text to scrub, and no new column to remember next sprint.

The boundary I would state plainly: this is the right answer for development, testing, demos, and load work, which is where the bulk copies live. It is not an analytics replacement, and it does not reproduce a specific customer's specific defect. Keep a controlled, logged path for that one case, and stop using it as the justification for keeping full copies everywhere.

One question

Take your masking script and your current schema. Count the columns in the schema. Count the columns the script touches.

That gap is your actual exposure, and it grew last sprint without anyone deciding it should.