Skip to content

PII masking

Requires a license with the pii-mask feature. See pricing.

PII masking replaces personal data with a placeholder before it leaves the instance, so ada@example.com in a response becomes [redacted-email]. It masks log lines too, and keeps a log of who received a response that carried personal data. What is stored stays as it was written. Only what goes out is masked.

Every response body from the Content API (/api/v1/*) and the Admin API passes through the rules, in priority order, lowest first. Each match is replaced. When a signed-in caller receives a response that matched, the access log records the caller and the kinds of data it held. Log messages and their string fields go through the built-in rules before they are written. Your own rules apply to responses only.

RuleReplaced withPriority
TOKEN[redacted-token]5
EMAIL[redacted-email]10
PHONE[redacted-phone]20
IP[redacted-ip], for IPv4 addresses30
CREDIT_CARD[redacted-cc], only for numbers that start with a card issuer's prefix and pass the Luhn check40
SSN[redacted-ssn]50

Bodies of type JSON, XML, form data, JavaScript and any text/* type are scanned. Server-sent event streams and binary bodies pass through.

Some Admin API paths return real values, because the console needs them to do its job: /api/admin/auth, /api/admin/users, /api/admin/invitations, /api/admin/gdpr, /api/admin/api-keys and /api/admin/admin-tokens. Reads on those paths are still recorded in the access log. The Content API has no such exceptions, and no role sees unmasked output on a masked path.

Matching is by pattern and cannot catch every form of personal data. Keep personal data out of fields that do not need it.

This adds a rule for staff numbers and watches it mask a response. You need an admin token in TOKEN, and a content type to write to, such as the post type from the quickstart, which also shows how to get a token.

  1. Test the pattern on a sample. Nothing is saved:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/pii/rules/test \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"pattern": "EMP-[0-9]{6}", "replacement": "[redacted-employee]", "sample": "Owner EMP-004211 approved"}'
    {"valid": true, "matches": ["EMP-004211"], "count": 1, "masked": "Owner [redacted-employee] approved"}

    A pattern that is not valid answers 200 with "valid": false and an error.

  2. Save it as a rule. A rule is off until you send "enabled": true:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/pii/rules \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"name": "EMPLOYEE_ID", "description": "Staff numbers", "pattern": "EMP-[0-9]{6}", "replacement": "[redacted-employee]", "enabled": true}'

    The answer is 201 with the rule, its id and "priority": 100. Copy the id into RULE_ID.

  3. Write an entry that carries a staff number:

    Terminal window
    curl -X POST http://localhost:3002/api/v1/content/post \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"data": {"title": "Approved by EMP-004211"}}'

    The entry is stored as written, but the answer already reads "title": "Approved by [redacted-employee]". Every later read is masked the same way.

  4. See the access log:

    Terminal window
    curl "http://localhost:3001/api/admin/pii/access-log?limit=5" \
    -H "Authorization: Bearer $TOKEN"

    The newest entry names you as viewer_user_id, your role, and "pii_type": "EMPLOYEE_ID".

  5. Remove the rule:

    Terminal window
    curl -X DELETE http://localhost:3001/api/admin/pii/rules/$RULE_ID \
    -H "Authorization: Bearer $TOKEN"

    The answer is 204.

A super admin manages the same rules and reads the log under Settings > PII masking in the admin console.

A tenant can hold up to 50 enabled rules of its own. A custom rule runs after the built-in rules unless you give it a lower priority. A custom rule with the same name as a built-in rule replaces it, so a disabled rule named EMAIL turns email masking off for that tenant. A saved rule applies to the next response, with no restart.

FieldRuleDefault
nameRequired, up to 255 charactersnone
descriptionOptionalempty
patternRequired, an RE2 regular expression up to 512 characters. It must not match the empty stringnone
replacementRequired, up to 255 charactersnone
priorityLower runs first. 0 or missing means the default, and a negative number is refused100
enabledWhether the rule runsfalse

RE2 runs in linear time, so no pattern can stall a response. PUT replaces a rule, so send every field.

GET /api/admin/pii/access-log takes limit (default 50, at most 500) and offset. Each entry carries viewer_user_id, viewer_role, pii_type (one rule name, or several joined by commas), accessed_at and tenant_id. An admin sees their own tenant. A super admin sees every tenant.

Entries older than PII_MASK_RETENTION_DAYS are deleted every hour. Deleting a tenant deletes its entries.

VariableWhat it doesDefault
PII_MASK_RETENTION_DAYSDays the access log keeps an entry. 0 or less keeps entries forever.90

If you set LYEVE_PLUGINS, include pii-mask. See Licensing and tiers.

All routes need the admin role and act on the caller's tenant.

Rules and access log
MethodPathPurpose
GET/api/admin/pii/rulesThe built-in rules (presets), the tenant's own (custom) and the limits.
POST/api/admin/pii/rulesAdd a rule.
POST/api/admin/pii/rules/testTry a pattern on a sample of up to 4096 characters.
PUT/api/admin/pii/rules/{id}Replace a rule. Send every field.
DELETE/api/admin/pii/rules/{id}Remove a rule. Answers 204.
GET/api/admin/pii/access-logWho received responses that carried personal data.
StatusMessageCause
422invalid rule: pattern does not compile as RE2: ...The pattern is not valid RE2.
422invalid rule: pattern matches the empty string, which would redact every responseThe pattern would match everywhere.
422this tenant already has the maximum number of enabled rules50 rules are already enabled.
Every error
StatusMessageCause
422sample is longer than 4096 charactersThe test sample is too long.
400body must be a JSON object describing one ruleThe body is not JSON or has unknown fields.
404failed to update the rule or failed to delete the ruleNo rule with that id in this tenant.
402payment_requiredThe license no longer carries pii-mask.