Data masking
Anonymize PII on branch creation — masking policies, the built-in function library, and how masked branches stay sealed until the mask commits.
Data masking anonymizes sensitive columns the moment a branch is created. You attach a masking policy to the branch-create call, and the new branch's matching columns are irreversibly rewritten — hashed, nulled, truncated, or replaced — before the branch is ever reachable. The parent branch is never modified.
You'd reach for it to hand production-shaped data to development, CI, or a contractor without handing over the PII inside it: real table shapes, real row counts, fake emails.
How it works
A masking policy is a project-scoped, named set of rules. Each rule matches columns by pattern and names a built-in masking function:
| Field | Meaning |
|---|---|
schema_pattern | Schema name pattern (% or * = any, _ = one character). Defaults to *. |
table_pattern | Table name pattern. |
column_pattern | Column name pattern. |
masking_fn | One of the built-in functions below. |
fn_args | Function arguments (only mask_constant takes one: value). |
At branch-create time, when a masking_policy_id is supplied:
- The branch forks from its parent as usual (copy-on-write — instant).
- The branch enters the
maskingstate. No endpoint is provisioned and the proxy refuses connections to it: there is no window in which the pre-masked data can be read. - A masking worker connects to the branch's compute as a least-privilege role, discovers the schema, matches your rules against it, and runs every rewrite in a single transaction.
- The branch lands in
readyand its endpoints come up — now serving only masked data. On any failure the branch lands infailedand stays sealed; delete it and retry.
Rule inputs are treated as data, never as SQL: identifiers are quoted and argument values are bound as parameters at execution, so a hostile column name or constant cannot break out of the rewrite.
Built-in functions
| Function | Effect |
|---|---|
mask_email | md5(value)@masked.invalid |
mask_name | Name_ + an 8-char hash prefix |
mask_null | NULL (column must be nullable) |
mask_constant | A fixed value you supply (fn_args.value) |
mask_ssn_partial | ***-**-1234 — keeps the last 4 digits |
mask_credit_card | ****-****-****-1234 — keeps the last 4 digits |
mask_ip | 0.0.0.0 |
mask_date_year | Keeps the year, truncates to Jan 1 |
mask_hash | md5(value) |
mask_shuffle | Hash-based scramble (full character shuffle is planned) |
mask_phone | A synthetic +1-555-XXXX number |
mask_uuid | A fresh random UUID |
The catalog is also served by the API:
curl -s https://api.test.kisenon.com/v1/masking-functions \
-H "Authorization: Bearer $KISENON_API_KEY"Managing policies
Data masking is rolling out. The policy editor and the branch-create dropdown described here are being enabled progressively; until masking is live for your account the API answers
501 not_implementedand the console shows a "not yet available" notice. Your projects are unaffected in the meantime.
In the console, open Project settings → Data masking to create a policy: name it, add rules (pattern columns + a function dropdown), and save. The same surface exists on the API:
curl -s -X POST \
https://api.test.kisenon.com/v1/projects/$PROJECT_ID/masking-policies \
-H "Authorization: Bearer $KISENON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "dev-safe",
"rules": [
{"table_pattern": "users", "column_pattern": "email", "masking_fn": "mask_email"},
{"table_pattern": "users", "column_pattern": "phone", "masking_fn": "mask_null"},
{"table_pattern": "%", "column_pattern": "%ssn%", "masking_fn": "mask_ssn_partial"}
]
}'Then create a masked branch by adding the policy to a normal branch-create call:
curl -s -X POST \
https://api.test.kisenon.com/v1/projects/$PROJECT_ID/branches \
-H "Authorization: Bearer $KISENON_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "masked-dev", "masking_policy_id": "'$POLICY_ID'"}'The branch reports state: "masking" while the rewrite runs; watch it
flip to ready in the console (live, via the project
event stream) or by polling GET /v1/branches/{id}.
Good to know
- Masking is one-shot, at creation. Editing a policy never touches branches that were already created with it — they keep the data shape they were born with. Re-create the branch to apply the new rules.
- A policy in use can't be deleted. Deleting a policy that a live
branch was created with returns
409 policy_in_use; delete those branches first. - Unmatched rules are skipped, not fatal. If your schema drifted and a pattern matches nothing, the branch still completes — check the rule patterns if a column you expected masked still has real data shape.
- Type mismatches fail the branch. A rule that matches a column its
function can't rewrite (say
mask_emailon aninteger) aborts the whole mask — the branch lands infailedand is never exposed half-masked.