kisenon

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:

FieldMeaning
schema_patternSchema name pattern (% or * = any, _ = one character). Defaults to *.
table_patternTable name pattern.
column_patternColumn name pattern.
masking_fnOne of the built-in functions below.
fn_argsFunction arguments (only mask_constant takes one: value).

At branch-create time, when a masking_policy_id is supplied:

  1. The branch forks from its parent as usual (copy-on-write — instant).
  2. The branch enters the masking state. No endpoint is provisioned and the proxy refuses connections to it: there is no window in which the pre-masked data can be read.
  3. 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.
  4. The branch lands in ready and its endpoints come up — now serving only masked data. On any failure the branch lands in failed and 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

FunctionEffect
mask_emailmd5(value)@masked.invalid
mask_nameName_ + an 8-char hash prefix
mask_nullNULL (column must be nullable)
mask_constantA 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_ip0.0.0.0
mask_date_yearKeeps the year, truncates to Jan 1
mask_hashmd5(value)
mask_shuffleHash-based scramble (full character shuffle is planned)
mask_phoneA synthetic +1-555-XXXX number
mask_uuidA 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_implemented and 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_email on an integer) aborts the whole mask — the branch lands in failed and is never exposed half-masked.