Mask Sensitive Columns with a View
Expose masked columns through a view for support and analytics roles, grant access to the view instead of the table, and keep the raw table restricted.
Task
Give support and analytics read-only access to masked data without touching the base table.
When to use this
Support and analytics need to see records, but granting table access exposes full PII that they do not need.
Prerequisites
- PostgreSQL views and roles
- A defined masking rule per sensitive column
Steps
1. Create a view that selects non-sensitive columns and masks or partially masks the rest.
Expected output: Sensitive values are not visible in full.
2. Grant SELECT on the view to the support and analytics roles only.
Expected output: Those roles never need table privileges.
3. Revoke direct table access from those roles.
Expected output: The raw table stays restricted.
4. Document the masking and verify it with a query as the restricted role.
psql -c "GRANT SELECT ON customers_masked TO support_role; REVOKE SELECT ON customers FROM support_role;"Expected output: Masking is provable, not assumed.
Troubleshooting
Problem
Masking bypassed via the base table
Solution
Revoke table privileges; a view is only a boundary if the underlying table is not directly readable.
Problem
Masking too weak
Solution
Mask enough to prevent re-identification, not just the last digits of a single field.
Limitations
- A masking view protects reads through the view; it does not change what the application itself stores.
- Some masking needs row-level context, which requires a function rather than a static view.
Context
Give support and analytics read-only access to masked data without touching the base table. Support and analytics need to see records, but granting table access exposes full PII that they do not need.
Steps
- Create a view that selects non-sensitive columns and masks or partially masks the rest. Expected: Sensitive values are not visible in full.
- Grant SELECT on the view to the support and analytics roles only. Expected: Those roles never need table privileges.
- Revoke direct table access from those roles. Expected: The raw table stays restricted.
- Document the masking and verify it with a query as the restricted role. Expected: Masking is provable, not assumed.
CREATE VIEW customers_masked AS
SELECT id, tenant_id,
left(email, 1) || '***@' || split_part(email, '@', 2) AS email,
regexp_replace(phone, '[0-9]', '*', 'g') AS phone
FROM customers;
REVOKE SELECT ON customers FROM support_role;
GRANT SELECT ON customers_masked TO support_role;
Verify
- Sensitive values are not visible in full.
- Those roles never need table privileges.
- The raw table stays restricted.
- Masking is provable, not assumed.
Limits
- A masking view protects reads through the view; it does not change what the application itself stores.
- Some masking needs row-level context, which requires a function rather than a static view.