TABLE OF CONTENTS
- •Is there real personal data in your test environments?
- •Static and Dynamic Masking at a Glance
- •Why Masking Is Necessary
- •Step 1: Find the Personal Data
- •Step 2: Choose the Masking Method
- •Static Masking for Test Environments
- •Dynamic Masking for Production
- •The built-in method: views and column privileges
- •Extensions
- •Ways Masking Can Be Bypassed
- •Checklist for the Masking Process
- •Data Masking with Aryasoft
- •Frequently Asked Questions
- •Sources
- •Clear personal data out of your test environments
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.
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.
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
Sources
The technical details in this guide were checked against the following sources in November 2026.
- PostgreSQL documentation: CREATE VIEW
- PostgreSQL documentation: Privileges
- PostgreSQL Anonymizer documentation
- AWS: Using dynamic masking with Aurora PostgreSQL (pg_columnmask)
- 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.