Database with masked record cards

Data Masking in PostgreSQL: Handling Sensitive Data in Test Environments

Static and dynamic data masking in PostgreSQL: finding personal data, masking methods, masking scripts for test environments, view-based masking and extensions.

November 11, 2026

Data masking means showing personal and sensitive data as altered, partly hidden or fake values instead of the real ones. PostgreSQL offers two main approaches: static masking, where the data copied to test and development environments is changed permanently, and dynamic masking, where the data stays in place and is shown masked only to users without the right permissions. Copying production data to a test environment without masking it is one of the most common data protection risks we see.

This guide is for development teams that feed test environments with production data and for IT managers responsible for compliance with data protection laws such as GDPR and KVKK (Türkiye’s data protection law). We tested the examples on PostgreSQL 18.

PostgreSQL Security

Is there real personal data in your test environments?

Aryasoft finds the personal data in your databases and builds a repeatable process that feeds masked data to your test environments.

Contact Us

Static and Dynamic Masking at a Glance

Static masking Dynamic masking
What it does Permanently changes a copy of the data. Leaves the data unchanged and masks query results per user.
Where it’s used Test, development, training and external sharing environments Support, reporting and call center teams in production
Does real data stay in the environment? No Yes; authorized users see the real data.
Risk If masking is incomplete, personal data remains in the copy. Real data can be seen through a permission mistake or a bypass technique.
Typical tools SQL scripts, PostgreSQL Anonymizer Views and column privileges, PostgreSQL Anonymizer, pg_columnmask on Aurora

Why Masking Is Necessary

Restoring a copy of the production database into a test environment is a common way to give developers realistic data. But test environments are rarely protected as well as production: wider access, weaker passwords, external developers and queries nobody logs. Real personal data in these environments makes every security measure in production pointless.

Data protection laws require personal data to be processed only as far as is relevant, limited and proportionate to the purpose. A software test doesn’t need real customers’ national ID numbers.

Warning

Masking isn’t the same as anonymization. Masked data can still count as personal data if, combined with other data, it lets someone re-identify a person. Assess the legal status of test data together with your organization’s data protection officer.

Step 1: Find the Personal Data

The first step in masking is knowing which columns hold personal or sensitive data. Column names are a good place to start. The pattern below includes common Turkish column names alongside English ones:

SELECT table_schema, table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
  AND column_name ~* '(tckn|tc_?kimlik|kimlik|ad_?soyad|full_?name|e_?mail|eposta|telefon|phone|gsm|adres|address|iban|kart|card|dogum|birth|maas|salary)'
ORDER BY table_schema, table_name, column_name;

Column names aren’t enough on their own; free-text fields such as “description” or “notes” can also hold personal data. Run pattern checks on sample data in these fields (for example 11-digit numbers or email addresses). Turn the findings into an inventory table that shows which column is masked with which method.

Step 2: Choose the Masking Method

Method Example When
Partial hiding 12345678901 → *******8901 When support teams need to verify a record
Replacement with a fake value Jane Smith → Customer 1047 When test data only needs to look realistic
Consistent pseudonym The same email always maps to the same fake value When relationships and joins across tables must be preserved
Adding noise Salary changes randomly by ±10% When the statistical distribution must be preserved
Generalization Date of birth → year of birth When analysis doesn’t need the exact value
Deletion (NULL) The notes field is emptied When tests don’t need the field

Static Masking for Test Environments

The simplest and most transparent method is to run a masking script on the copy restored to the test environment. The script must run automatically on every restore and finish before the copy is opened to users.

-- Runs on the TEST copy right after the restore, before anyone gets access
UPDATE customers
SET full_name = 'Customer ' || id,
    email     = 'customer' || id || '@example.test',
    phone     = '0500' || lpad((id % 10000000)::text, 7, '0'),
    tckn      = NULL;

-- Consistent pseudonym: the same input always maps to the same output
UPDATE orders
SET customer_email = 'u_' || left(md5(customer_email || 'project-secret-salt'), 12) || '@example.test';

The salt used for consistent pseudonyms must stay secret; otherwise common email addresses can be tried until they match.

Tip

Make the masking script an integral part of the job that refreshes the test environment (the restore job), and run validation queries at the end of the script: check that no row in a column that should be masked still matches the real pattern. If validation fails, the environment shouldn’t be opened to users.

Dynamic Masking for Production

The built-in method: views and column privileges

With PostgreSQL’s built-in features, dynamic masking is done by creating a masked view and granting users access to the view instead of the table:

CREATE TABLE customers (id int PRIMARY KEY, full_name text, tckn text, phone text);
INSERT INTO customers VALUES (1, 'Ayşe Yılmaz', '12345678901', '05321234567');

CREATE VIEW customers_masked AS
SELECT id,
       left(full_name, 1) || '*** ' || left(split_part(full_name, ' ', 2), 1) || '***' AS full_name,
       repeat('*', 7) || right(tckn, 4) AS tckn,
       left(phone, 4) || ' *** ** ' || right(phone, 2) AS phone
FROM customers;

CREATE ROLE support_user LOGIN PASSWORD 'change_me';
REVOKE ALL ON customers FROM support_user;
GRANT SELECT ON customers_masked TO support_user;

-- As support_user:
SELECT * FROM customers_masked;
--  id | full_name | tckn        | phone
--   1 | A*** Y*** | *******8901 | 0532 *** ** 67

This method is simple and transparent, but the application has to query the view instead of the table, and masking rules are managed separately in each view.

Extensions

Tool What it is Note
PostgreSQL Anonymizer An open source extension. Declares masking rules as labels at column level and offers dynamic masking, static masking and masked exports. Installed on your own server; may not be available on some managed cloud services.
pg_columnmask A dynamic masking extension specific to Amazon Aurora PostgreSQL; policies apply by role and weight. Aurora PostgreSQL only (16.10+ and 17.6+); not in the community edition

Whichever tool you choose, the masking rules need to be tied to an inventory, with a process that adds new columns to that inventory.

Ways Masking Can Be Bypassed

Dynamic masking is safe only if an unauthorized user has no path to the underlying data. Check these points:

  • Direct table access: the role that uses the masked view must have no privileges on the base table.
  • Inference attacks: if filters like WHERE tckn LIKE '1%' work on a masked column, the value can be worked out by guessing.
  • Backups and exports: backups contain the real data; access to them must be restricted and they must be encrypted.
  • Logs: query logs can contain real values as parameters.

Checklist for the Masking Process

Step Owner Evidence
A personal data inventory has been produced. Data owner + DBA Column inventory
A method has been chosen for each column. Data owner + data protection officer Masking rules
Masking has been added to the test refresh job. DBA Job definition and script
Validation runs after masking. DBA Validation queries and results
The test environment isn’t opened before masking finishes. DBA Workflow
New columns are added to the inventory. Development team Change management process

Data Masking with Aryasoft

Aryasoft finds the personal data in your PostgreSQL and SQL Server environments, sets column-level masking rules with you and builds a process that automatically refreshes test environments with masked data. We share our field observations on data governance in our data governance post, and PostgreSQL’s other security layers in our PostgreSQL security guide. Details are on our PostgreSQL services page.

Frequently Asked Questions

Under the principle that personal data is processed only as far as limited and proportionate to the purpose, masked or synthetic data should be used when testing doesn’t need real personal data. Make the assessment for your organization together with your data protection officer.
They solve different problems. Static masking is for test and development environments; dynamic masking is for users with limited permissions in production. Most organizations need both.
Static masking runs only when the test environment is refreshed and doesn’t touch production. With dynamic masking, the masking functions run on every query, which can add a small overhead on large result sets.
Not always. If the data, combined with other information, lets someone re-identify a person, it remains personal data. Full anonymization needs stricter methods.

Sources

The technical details in this guide were checked against the following sources in November 2026.

  1. PostgreSQL documentation: CREATE VIEW
  2. PostgreSQL documentation: Privileges
  3. PostgreSQL Anonymizer documentation
  4. AWS: Using dynamic masking with Aurora PostgreSQL (pg_columnmask)
  5. Turkish Personal Data Protection Authority (KVKK)

Data Masking

Clear personal data out of your test environments

Aryasoft finds personal data, sets masking rules with you and builds a process that refreshes test environments with masked data automatically.

4.7/5