Skip to main content

MySQL Dynamic Data Masking

Tianzhou · May 14, 2026

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 years, MySQL offered two paths: a function library in Enterprise, an OSS clone from Percona — both wired through views. MySQL 9.7 LTS (April 21, 2026) adds a third: a first-class masking policy object inside the server. MariaDB takes a different route entirely — masking runs outside the server, in MaxScale, the MariaDB connection proxy. Five options in total. This post compares them.

MySQL 9.7 Enterprise Dynamic Data Masking

Dynamic Data Masking (DDM) is GA in MySQL 9.7 Enterprise Edition and OCI MySQL HeatWave. Define a policy. Attach it to a column. The server enforces it.

-- 1. Define the policy. The predicate decides who sees cleartext.
CREATE MASKING POLICY mask_ssn_policy(ssn_col)
  CASE WHEN CURRENT_USER_IN('seeall')
       THEN ssn_col
       ELSE mask_ssn(ssn_col)
  END;

-- 2. Attach the policy to the column.
ALTER TABLE protected.user_profiles
  ALTER COLUMN ssn
  SET MASKING POLICY mask_ssn_policy;

-- 3. Every read goes through the policy.
SELECT id, first_name, last_name, ssn FROM protected.user_profiles;
-- noseeall → XXX-XX-0001   (masked)
-- seeall   → 900-01-0001   (cleartext)

Two access predicates ship in 9.7: CURRENT_USER_IN(...) and CURRENT_ROLE_IN(...). The policy body uses the existing MySQL masking function library — mask_inner, mask_outer, mask_pan, mask_ssn, gen_rnd_email, gen_rnd_us_phone. The functions are not new. The policy object is. Before 9.7, binding a masking function to a column meant a view or an in-query call. In 9.7, the server does the binding.

What the policy object adds:

  • Server-side enforcement. Interactive SQL, application traffic, mysqldump, downstream extracts — every read path returns the masked value. Direct connections to the base table cannot bypass it.
  • No view inventory. One policy replaces a per-variation view catalog.
  • Predicate safety. Masking applies inside WHERE and JOIN. The WHERE ssn LIKE '900-%' side-channel is closed.

What it does not do:

  • Row-level filtering. DDM gates columns, not rows. For row-level control, pair with row-level filtering.
  • Cover Community or Percona. Enterprise and HeatWave only.

MySQL Enterprise Data Masking Plugin (legacy)

Before 9.7, the only Enterprise path was the data masking plugin, shipped since 5.7. The plugin exposes masking functions (mask_inner, mask_outer, mask_ssn, …). It does not expose a policy object. Protect raw data by wrapping the base table in a view that calls those functions, then grant access to the view.

_

The pattern still works on 8.0 and 8.4. Documented limits:

  • Account model first. Enforcement keys off MySQL roles. Most instances have a handful of users. Adopting the plugin means redesigning accounts before policies.
  • One view per variation. The view inventory grows with every variation. Schema changes ripple through it.
  • No management surface. It is plain SQL. Version, review, and audit it yourself.
  • Base-table bypass. Anyone with SELECT on the base table sees cleartext.

On 9.7 Enterprise, the policy object replaces this pattern for new work. The functions remain for direct calls.

Percona Data Masking Plugin

Percona Data Masking is the open-source clone of the legacy Enterprise plugin, shipped with Percona Server for MySQL.

_

Same function library. Same view-based pattern. Same limits. Free, but only on Percona Server. As of 9.7, Percona has not implemented CREATE MASKING POLICY. The OSS path is still views over functions.

MariaDB MaxScale Masking Filter

MariaDB Server has no native masking. No MASKING POLICY object. No mask_inner / mask_ssn plugin like MySQL Enterprise. Masking runs outside the server — in MaxScale, the MariaDB connection proxy, whose Masking filter rewrites result rows on the wire.

9.7 DDM, the legacy plugin, and Percona all run inside the server. MaxScale runs in front of it — the first of two outside-the-engine options compared here. Bytebase is the other.

Rules are JSON, not DDL:

{
  "rules": [
    {
      "replace":    { "database": "hr", "table": "person", "column": "ssn" },
      "with":       { "value": "XXX-XX-0000", "fill": "X" },
      "applies_to": [ "'analyst'@'%'" ],
      "exempted":   [ "'security_officer'@'%'" ]
    }
  ]
}

Two operations. replace substitutes the column with a fixed value or a repeated fill character. obfuscate runs the value through a non-reversible hash. Both accept a match field for PCRE2 partial replacement.

MaxScale ships behaviour flags to close the obvious bypasses — prevent_function_usage rejects CONCAT(ssn, ''), treat_string_arg_as_field blocks ANSI_QUOTES tricks, check_subqueries rejects subqueries that read the masked column.

What MaxScale documents about its own filter:

  • Best-effort, not malicious-defense. The filter is "intended for protecting against accidental misuse rather than malicious attacks."
  • Direct connections bypass it. Anyone with credentials to the MariaDB instance behind MaxScale can connect on the native port and read cleartext. The deployment must remove direct routes.
  • Narrow type coverage. Binary, text, and enumeration columns only. Numeric columns are not maskable by the filter.
  • Known bypass vectors. Intermediate tables built from SELECT INTO, expressions that wrap the masked column, user variables, UNION against unmasked sources. The hardening flags reduce these, not eliminate them.
  • Licensing. MaxScale ships under Business Source License 1.1. Source-available. Free for limited production use. Converts to GPLv2 four years after release. Not OSS in the OSI sense.

Bytebase Dynamic Data Masking

_

Like MaxScale, Bytebase lives outside the engine. Unlike MaxScale, Bytebase is not a wire-protocol proxy. MaxScale is a transparent proxy: apps, BI tools, and scheduled jobs point at its port and see masked data unchanged. Bytebase is a workflow surface: humans log into the SQL Editor; queries route through approval and audit; service traffic continues straight to the database. Same family. Different surfaces.

Native MySQL masking is fragmented. 9.7 DDM: Enterprise + HeatWave only. Legacy plugin: Enterprise only. Percona's clone: Percona Server only. Community: nothing. Real fleets span several of these — plus RDS, Aurora, and managed forks that ship none. Three of the four native paths also require views, multiplied per masking variation.

Bytebase Dynamic Data Masking applies one policy model across every MySQL distribution. No view inventory. No edition gate. No per-engine plugin. Policy changes and exemption requests run through a built-in workflow — Request. Review. Approve. — every step audited.

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

  1. Global Masking Rule. Workspace-level. Rules evaluate top-down. First match wins. Match conditions span environment, project, database, and data classification. Each match applies a Semantic Type, which selects a masking algorithm — full, partial, MD5, range, or custom. One rule covers the Enterprise 9.7 cluster, the Percona shard, and the Community staging instance.

_

  1. Column Masking. Project-level override on a specific column when the global rule does not apply.

_

  1. Masking Exemption. Named users receive time-bound Query or Export exemptions to specific databases or tables. Service accounts are not eligible. Every grant logged. Every access logged.

_

Masking is infectious. When a column is masked, the policy propagates to every view and derived structure that depends on it. The view-inventory problem all three native MySQL options share disappears.

_

Policies can also be codified via GitOps.

Masking decisions are recorded in the audit log. Every SQL execution entry carries per-column masking metadata — masked columns, Semantic Type, matching rule — alongside user, source IP, statement, and row count. Granted exemptions, used exemptions, and policy edits are first-class audit events.

Enforcement boundary: Bytebase masks queries routed through the SQL Editor. Direct connections bypass it. Route human access through Bytebase and one policy applies across MySQL Community, Enterprise, Percona, MariaDB, RDS, Aurora, and managed forks — including the variants where neither the plugin, 9.7 DDM, nor MaxScale reaches.

Comparison

MySQL 9.7 Enterprise DDMEnterprise Plugin (legacy)Percona PluginMariaDB MaxScale MaskingBytebase Dynamic Data Masking
CompatibilityMySQL 9.7+ Enterprise, HeatWaveMySQL EnterprisePercona Server for MySQLMariaDB behind MaxScaleAll MySQL/MariaDB distributions ⭐️
MechanismPolicy object on column ⭐️Functions + viewsFunctions + viewsJSON rules in proxyPolicy in Bytebase, applied at SQL Editor
Enforced atDatabase, every read path ⭐️View boundaryView boundaryProxy boundarySQL Editor
Policy mgmtFirst-class server objectsDIY viewsDIY viewsJSON config filesCentralized UI, grants, audit log ⭐️
WorkflowDDL onlyDDL onlyDDL onlyConfig-file editsRequest. Review. Approve. ⭐️
Row-level filterNo (pair with row-level filtering)NoNoNoNo (pair with access policy)
PricePaidPaidFree ⭐️BSL (source-available)Paid

Picking one

  • On 9.7 Enterprise or OCI HeatWave. Use the policy object. Only option that enforces uniformly across SELECT *, mysqldump, and direct client sessions.
  • On 8.0 or 8.4 Enterprise. Use the legacy plugin with views. Plan the migration to policies on the next LTS upgrade.
  • On Percona Server, no Enterprise budget. Use the Percona plugin. Same constraints as the legacy Enterprise plugin.
  • On MariaDB. Use the MaxScale masking filter — best-effort, by its own docs. Network-segment the MariaDB port so direct connections are not reachable. For stronger guarantees, route human access through Bytebase.
  • Mixed MySQL/MariaDB fleet, or alongside Postgres, Snowflake, and managed forks. Use Bytebase. One policy model. Every engine. Audited grants for every unmask.

Try Bytebase Dynamic Data Masking with this tutorial.

Back to blog

Explore the standard for database development