# Postgres Dynamic Data Masking

> Compare PostgreSQL data masking options — PostgreSQL Anonymizer 2.0 (Rust rewrite, five strategies, security labels) and Bytebase fleet-wide masking.

Tianzhou | 2026-05-14 | Source: https://www.bytebase.com/blog/postgres-dynamic-data-masking/

---

Sensitive columns — SSNs, credit cards, emails, addresses — must stay queryable for support, analytics, and development. Broad cleartext access is not the answer. Data masking is. For some workloads (GDPR, HIPAA, PCI), masking is also a legal requirement.

Postgres ships no masking primitives in core. Two options dominate the practical landscape: **PostgreSQL Anonymizer**, a community extension that lives inside the database, and **Bytebase Dynamic Data Masking**, a policy layer in front of it. This post compares them.

## PostgreSQL Anonymizer

[PostgreSQL Anonymizer](https://postgresql-anonymizer.readthedocs.io/) is a community extension by Damaen and the team at Dalibo. [Version 2.0](https://www.postgresql.org/about/news/postgresql-anonymizer-20-better-faster-safer-2993/) is a full rewrite in Rust on top of the [PGRX](https://github.com/pgcentralfoundation/pgrx) framework — improved memory safety, lower overhead, and a cleaner extension surface than the SQL/PL-pgSQL ancestor.

![_](/content/blog/postgres-dynamic-data-masking/postgresql-anonymizer.webp)

Masking configuration is stored in [PostgreSQL Security Labels](https://www.postgresql.org/docs/current/sql-security-label.html). The `pg_seclabel` mechanism was designed for security modules, but [it generalizes](https://www.pgedge.com/blog/custom-properties-for-postgresql-database-objects-without-core-patches) into a transactional, MVCC-aware way to attach custom metadata to any database object — no core patches required. Anonymizer pioneered this pattern for masking; pgEdge applies the same primitive to replication metadata. The mechanism is solid.

### Five strategies

Anonymizer 2.0 ships five distinct masking strategies. Pick by access pattern, not by aesthetics.

- **Dynamic Masking.** A role is declared `MASKED`. Sessions running under that role see masked output; other roles see cleartext. Role-based, transparent, no application changes.
- **Static Masking.** Rewrite the data in place. Destructive — only for clones and lower environments. Slow on large tables; Postgres rewrites every row containing at least one masked column.
- **Anonymous Dumps.** `pg_dump_anon` emits an anonymized dump for downstream consumers. The original cluster stays clean.
- **Masking Views.** Views over base tables that apply masking expressions. Grant access to the views, not the tables.
- **Masking Data Wrappers.** Foreign data wrappers that mask on read across federated sources.

### Masking transformations

Anonymizer 2.0 bundles eight transformation families: **substitution, randomization, faking, pseudonymization, partial scrambling, shuffling, noise addition, generalization**. The 2.0 faker is noticeably more realistic than 1.x — the locale-aware data generators replace the older static dictionaries.

```sql
-- 1. Load the extension and declare a masked role.
CREATE EXTENSION anon CASCADE;
SELECT anon.start_dynamic_masking();
CREATE ROLE analyst LOGIN PASSWORD '…';
SECURITY LABEL FOR anon ON ROLE analyst IS 'MASKED';

-- 2. Attach a masking rule via a security label on the column.
SECURITY LABEL FOR anon ON COLUMN customers.email
  IS 'MASKED WITH FUNCTION anon.fake_email()';

-- 3. analyst sees fake emails; postgres sees real ones.
SET ROLE analyst;
SELECT email FROM customers LIMIT 1;
--                email
-- ------------------------------
--  irma.fritsch@hahn-davis.test
```

### What's new in 2.0

- **Role-based transparent dynamic masking** — formerly tied to a single masked role; 2.0 supports nuanced role policy.
- **Multiple masking policies** — define and switch between policy sets per workload (e.g. one for analysts, another for support).
- **Anonymous dump as a first-class workflow** — not a bolt-on script.
- **Debian packages** — distribution-installable; no manual compile.

### Limitations to know

- **GUI client compatibility.** Per the docs, certain query patterns issued by tools like DBeaver or pgAdmin can [behave unexpectedly](https://postgresql-anonymizer.readthedocs.io/en/latest/dynamic_masking/) under dynamic masking.
- **View inventory.** Masking Views still grow with every variation. Schema changes ripple through the view set.
- **Per-extension, per-cluster.** Anonymizer runs inside one Postgres cluster. Across a fleet of Postgres + RDS + Aurora + Postgres-flavored clouds, you maintain the extension and the labels on each one.
- **Postgres only.** A heterogeneous fleet (Postgres + MySQL + Snowflake) needs a separate masking story for the non-Postgres engines.

## Bytebase Dynamic Data Masking

![_](/content/blog/mysql-dynamic-data-masking/bb-masking-overview.webp)

[Bytebase Dynamic Data Masking](https://docs.bytebase.com/security/data-masking/overview/) defines policies in Bytebase and enforces them at the SQL Editor query result. No extension. No security labels to maintain. One policy model across every PostgreSQL distribution — and every other engine in the fleet. Policy changes and exemption requests run through a built-in workflow — **Request. Review. Approve.** — with every step audited.

Policies are composed from three layers, evaluated in fixed precedence: **Masking Exemption > Global Masking Rule > Column Masking**.

1. **Global Masking Rule.** Workspace-level, ordered like iptables — the first matching rule wins. Match conditions span environment, project, database, and data classification level. Each match applies a Semantic Type, which in turn selects a masking algorithm (full, partial, MD5, range, or custom).

![_](/content/blog/mysql-dynamic-data-masking/bb-global-masking.webp)

2. **Column Masking.** Project-level override. `Project Owner` assigns a Semantic Type to a specific column when the global rule does not apply. Global rules still take precedence over column masking.

![_](/content/blog/mysql-dynamic-data-masking/bb-column-masking.webp)

3. **Masking Exemption.** Named users receive time-bound `Query` or `Export` exemptions to specific databases or tables. Service accounts are not eligible — exemptions are for human users querying through the SQL Editor. Every grant and every access is logged.

![_](/content/blog/mysql-dynamic-data-masking/bb-grant-exemption.webp)

Masking is **infectious**: when a column is masked, the policy propagates to every view and derived structure that depends on it. The result reaches the SQL Editor directly:

![_](/content/blog/mysql-dynamic-data-masking/bb-sql-editor-full-masking.webp)

_Policies can also be codified via [GitOps](https://github.com/bytebase/example-database-security)._

Masking decisions are recorded in the [audit log](/blog/bytebase-audit-logging/#what-bytebase-records). Every SQL execution entry carries per-column masking metadata — which columns came back masked, which Semantic Type triggered it, and which rule matched — alongside the user, source IP, statement, and row count. Granted exemptions, used exemptions, and policy edits are first-class audit events. The policy and the proof it was enforced live in the same record.

Enforcement boundary: Bytebase masks queries routed through the SQL Editor. Traffic that hits the database directly bypasses it. The operational pattern is to funnel human access through Bytebase — at which point masking applies uniformly across Postgres community, RDS, Aurora, Cloud SQL, AlloyDB, and managed forks.

## Comparison

|                  | PostgreSQL Anonymizer 2.0                       | Bytebase Dynamic Data Masking                       |
| ---------------- | ----------------------------------------------- | --------------------------------------------------- |
| Compatibility    | Postgres clusters with `anon` extension         | All PostgreSQL distributions ⭐️                    |
| Mechanism        | Security labels + extension functions ⭐️       | Policy in Bytebase, applied at SQL Editor          |
| Enforced at      | Database, every read path ⭐️                   | SQL Editor                                          |
| Strategies       | Dynamic, static, dumps, views, FDW ⭐️          | Dynamic only                                        |
| Policy mgmt      | SQL `SECURITY LABEL` per object                 | Centralized UI, grants, audit log ⭐️               |
| Workflow         | DDL only                                        | Request. Review. Approve. ⭐️                       |
| Row-level filter | No (pair with RLS)                              | No (pair with access policy)                        |
| Price            | Free ⭐️                                         | Paid                                                |

## Picking one

- **Single Postgres cluster, masking must enforce regardless of client.** Use PostgreSQL Anonymizer 2.0. Dynamic masking for analyst access; `pg_dump_anon` for environment clones; static masking only when the data really must be destroyed.
- **Mixed fleet — Postgres alongside MySQL, Snowflake, RDS, Aurora, or managed forks.** Use Bytebase. One policy model. Every engine. Audited grants for every unmask, recorded in the same place as your access logs.
- **Both.** Anonymizer handles destructive workflows (anonymized dumps, lower-environment refresh); Bytebase handles human query traffic with approval and audit. They compose.

---

Try Bytebase Dynamic Data Masking with [this tutorial](https://docs.bytebase.com/tutorials/data-masking/). Questions? Join our [Discord](https://discord.com/invite/huyw7gRsyA).