Most masking setups I’ve seen are a collection of policies applied to individual columns, each created manually, each slightly different from the one before, and nobody is quite sure which tables have actually been covered. Tag-based masking in Snowflake fixes that by inverting the problem: write one policy, bind it to a tag, and let dbt apply the tag wherever it needs to go. Security owns the policy, data engineering owns the column classification, and the two never have to coordinate on a column-by-column basis.
Three things to set up: a classification tag, a masking policy bound to that tag, and a dbt config that marks the columns. From that point on, every column carrying the tag gets masked automatically, with no extra steps.
1. Create the governance schema and tag
Run this as SECURITYADMIN. The dedicated schema keeps your governance objects separate from your data schemas, which matters when you add row access policies and other controls later. ALLOWED VALUES is worth using: it catches typos before they create untagged columns that slip through the cracks.
CREATE DATABASE IF NOT EXISTS master_governance;
CREATE SCHEMA IF NOT EXISTS master_governance.policies;
CREATE TAG IF NOT EXISTS master_governance.policies.data_class
ALLOWED VALUES ('PII', 'PCI', 'PUBLIC');
2. Create the masking policy and bind it to the tag
The policy is a simple role check: right role, you see the value; wrong role, you see a redaction string. The ALTER TAG ... SET MASKING POLICY line is what makes this scale. From that point on, setting the tag on any column anywhere in the account automatically triggers this policy on that column, no further configuration per table.
CREATE OR REPLACE MASKING POLICY master_governance.policies.pii_mask
AS (val STRING) RETURNS STRING ->
CASE
WHEN CURRENT_ROLE() IN ('SECURITYADMIN', 'HR_ANALYST') THEN val
ELSE '***MASKED***'
END;
ALTER TAG master_governance.policies.data_class
SET MASKING POLICY master_governance.policies.pii_mask;
3. Tag columns in dbt
With the Snowflake side set up, you classify columns in your schema.yml using the meta.snowflake_tags block. When dbt runs, a post-hook macro reads these entries and fires ALTER TABLE ... ALTER COLUMN ... SET TAG for each one. The classification lives in the same repository as your transformations, reviewed in the same pull request, visible in the same version history.
version: 2
models:
- name: stg_customers
description: "Staging table for customer profiles."
columns:
- name: customer_id
description: "Public internal identifier."
- name: social_security_number
description: "Government issued identifier."
meta:
snowflake_tags:
master_governance.policies.data_class: 'PII'
- name: credit_card_number
description: "Customer payment method."
meta:
snowflake_tags:
master_governance.policies.data_class: 'PCI'
The macro that reads these meta entries and fires the ALTER statements is straightforward to write, or you can adapt one of the community implementations. What matters is that once it is in place, no one runs manual DDL to protect a new column.
4. Audit your data estate
snowflake.account_usage.tag_references gives you a full inventory of every tagged object in the account. There is up to two hours of latency on this view, so it is the right place for compliance reporting, not for verifying a just-completed dbt run. For real-time confirmation after a run, query INFORMATION_SCHEMA.TAG_REFERENCES within the specific database.
SELECT
object_database,
object_schema,
object_name,
column_name,
tag_value
FROM snowflake.account_usage.tag_references
WHERE tag_name = 'DATA_CLASS'
AND tag_value = 'PII'
ORDER BY object_database, object_schema, object_name;
One policy.
One tag.
Every column your dbt project marks as PII, masked the moment it exists.
Full script
-- Step 1: governance schema and tag
CREATE DATABASE IF NOT EXISTS master_governance;
CREATE SCHEMA IF NOT EXISTS master_governance.policies;
CREATE TAG IF NOT EXISTS master_governance.policies.data_class
ALLOWED VALUES (‘PII’, ‘PCI’, ‘PUBLIC’);
– Step 2: masking policy bound to the tag
CREATE OR REPLACE MASKING POLICY master_governance.policies.pii_mask
AS (val STRING) RETURNS STRING ->
CASE
WHEN CURRENT_ROLE() IN (‘SECURITYADMIN’, ‘HR_ANALYST’) THEN val
ELSE ‘MASKED’
END;
ALTER TAG master_governance.policies.data_class
SET MASKING POLICY master_governance.policies.pii_mask;
– Step 4: audit PII columns across the account
SELECT
object_database,
object_schema,
object_name,
column_name,
tag_value
FROM snowflake.account_usage.tag_references
WHERE tag_name = ‘DATA_CLASS’
AND tag_value = ‘PII’
ORDER BY object_database, object_schema, object_name;