Tag-Based PII Masking
Estimated read: 2 min
Column masking has the same drift problem row-level security had before dynamic views: a manually applied mask, attached one column at a time with ALTER TABLE ... ALTER COLUMN ... SET MASK, only protects the columns someone remembered to mask on the day they wrote the migration script. A new ssn or date_of_birth column added six months later starts life completely unmasked until someone notices – and in a legacy migration with hundreds of tables, someone usually doesn’t notice until an auditor does.
Tag-based masking flips the trigger: instead of the mask being attached to a specific column, it’s attached to the tag, so any column carrying pii=true is masked the moment the tag is applied, whether the column existed at rollout or was added last week.
-- The masking function: format-preserving for analysts who need to eyeball values,
-- full redaction for everyone else
CREATE FUNCTION finance.governance.mask_pii(val STRING)
RETURN CASE
WHEN is_account_group_member('pii_reviewers') THEN val
WHEN is_account_group_member('analysts') THEN concat('***-**-', right(val, 4))
ELSE '***REDACTED***'
END;
-- An ABAC policy applies the function to every column tagged pii=true,
-- across every table in the schema, present and future
CREATE POLICY mask_finance_pii
ON SCHEMA finance.accounts_receivable
COLUMN MASK finance.governance.mask_pii
TO `account groups:all_users`
FOR TABLES
MATCH COLUMNS has_tag_value('pii', 'true') AS c
ON COLUMN c;
Notice the masking function itself supports tiered disclosure rather than a single on/off redaction: pii_reviewers see the real value, analysts see a partially masked value that’s still useful for pattern-matching (last four digits of an SSN), and everyone else sees a fixed redaction string. This maps directly onto the tiered access most legacy warehouses already had informally through separate “masked” and “unmasked” views – ABAC just makes the tiers a property of the data’s tag rather than of which view someone happened to query.
A masking function is only as trustworthy as the group membership it checks –
is_account_group_member('pii_reviewers')is a real security boundary only ifpii_reviewersmembership is tightly controlled and audited. Test every masking policy from an account that is deliberately not in the privileged group before sign-off, not just from an admin account where every path looks unmasked.
The payoff mirrors the earlier “500 roles to 12 tags” lecture: instead of maintaining a spreadsheet of which columns need which mask, the mask is data-driven from a single source of truth (information_schema.column_tags), and a compliance query – “show me every table with an unmasked PII column” – becomes a join instead of a manual audit. The next lecture covers the third leg of this stool: what happens when a query against masked or filtered data comes from an engine that isn’t Databricks at all.
| ← Previous: Dynamic Views and Row-Level Security | Next: Cross-Engine Access: Delta Sharing, Iceberg, Credential Vending → | |:—|—:|