PII masking¶
PII is personal data, such as an email address. Analysts query a customer table, but they never see the real email, phone or ssn.
Example¶
Start the server with examples/pii-masking/serve.sh. Each column has its own column mask. This one returns NULL for email.
apiVersion: sluice.bino.bi/v1alpha1
kind: ColumnMaskPolicy
metadata:
name: mask-email
priority: 90
spec:
match:
any:
- resources:
catalogs: ["shop"]
schemas: ["main"]
tables: ["customers"]
columns: ["email"]
mask:
type: "null"
The masks on phone and ssn select their column in the same way. Both return a constant.
The analyst key reads the table.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_analyst.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, email, phone, ssn, birth_date FROM shop.main.customers ORDER BY id"}'
{
"query_id": "<query-id>",
"columns": ["id", "email", "phone", "ssn", "birth_date"],
"rows": [
[1, null, "+1-555-REDACTED", "REDACTED", "1988-03-14"],
[2, null, "+1-555-REDACTED", "REDACTED", "1975-07-22"],
[3, null, "+1-555-REDACTED", "REDACTED", "1990-11-30"],
[4, null, "+1-555-REDACTED", "REDACTED", "2001-02-02"]
],
"row_count": 4,
"truncated": false
}
What happened: Sluice replaced the table with a subquery that returns the masked values. birth_date has no mask, so it comes back unchanged.
This suite shows the rewritten SQL.
# A masked table needs its column list. The test runner has no database.
schema:
shop.main.customers: [id, tenant_id, email, phone, ssn, birth_date, created_at]
cases:
- name: email, phone and ssn are masked
identity: { subject: pii-analyst, groups: [analytics] }
sql: "SELECT id, email, phone, ssn FROM shop.main.customers"
expect:
outcome: allow
masks:
- "shop.main.customers.email=null"
- "shop.main.customers.phone=constant"
- "shop.main.customers.ssn=constant"
rewrittenSql: >-
SELECT id, email, phone, ssn
FROM (SELECT sluice_src.id, sluice_src.tenant_id, NULL AS email, '+1-555-REDACTED' AS phone,
'REDACTED' AS ssn, sluice_src.birth_date, sluice_src.created_at
FROM shop.main.customers sluice_src) customers
More examples¶
A filter on a masked column sees the masked value. This search for alice finds nothing.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_analyst.supersecret" \
-H "Content-Type: application/json" \
-d @- <<'EOF'
{"sql": "SELECT id FROM shop.main.customers WHERE email LIKE '%alice%'"}
EOF
GROUP BY on a masked column groups the masked value.
curl -s http://localhost:8080/v1/query \
-H "X-Api-Key: sl_demo_analyst.supersecret" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT ssn, count(*) AS customers FROM shop.main.customers GROUP BY ssn"}'
{
"query_id": "<query-id>",
"columns": ["ssn", "customers"],
"rows": [
["REDACTED", 4]
],
"row_count": 1,
"truncated": false
}
Limits¶
- A mask without
resources.columnsmasks nothing. Sluice gives no warning. - Some mask types, for example
fake, run after the query. A filter, join orGROUP BYon such a column fails withERR_MASK_UNSUPPORTED_CONTEXT. See Column masks.
See also¶
- Column masks
- Testing policies: the
schema:block for masked tables - Break-glass access