Skip to main content

Data Masking (Client + Server)

CAIP Agents SDK masks sensitive data before it reaches Langfuse.

How masking works

  • Client-side masking: SDK applies regex-based masking to trace input/output and metadata in app code.
  • Server-side masking: Langfuse-side masking is also configured in your deployment, so data is protected again during ingestion.
  • Defense in depth: both layers run, reducing the chance of raw PII appearing in traces.

Default behavior

  • Masking is always on when Langfuse observability is enabled.
  • Replacement format is label-based redaction, for example: [REDACTED_EMAIL].
  • Built-in patterns include common sensitive data such as email, phone, credit card, API keys, JWT, and IDs.

Add your own masking rules

Use configure_masking() to extend or customize masking.

from caip_agents_sdk import configure_masking

configure_masking(
custom_patterns=[
("ORDER_ID", r"ORD-\d{8}-[A-Z]{2}"),
("INTERNAL_ID", r"INT-\d{6}"),
]
)

Add your own masking function

Use custom_hook for logic that regex alone cannot handle.

from caip_agents_sdk import configure_masking

def my_mask_hook(data):
if isinstance(data, str):
return data.replace("ACME Corp", "[COMPANY]")
return data

configure_masking(custom_hook=my_mask_hook)
note
  • custom_hook runs after regex masking.
  • Keep hooks deterministic and fast (they run for traced payloads).

Server-Side Masking Configuration

Server-side masking runs inside the Langfuse deployment as a sidecar on the worker pod. During ingestion, the Langfuse worker calls the sidecar, which redacts matches as [REDACTED_<label>] before the trace is persisted. This is the second, deployment-owned layer of defense that runs regardless of whether a client remembered to enable SDK-side masking.

Unlike client-side rules (which live in application code), server-side patterns are configuration: they are driven entirely by Helm values, so onboarding a new region or adding a rule requires no code or template changes.

Architecture

values.yaml -> masking.basePatterns (shared by ALL regions)
values-<region>.yaml -> masking.regionalPatterns (region-specific additions)


templates/masking-configmap.yaml
│ concatenates base + regional (base first, in file order)

ConfigMap: caip-langfuse-masking-patterns (key: patterns.yaml)
│ mounted at /config/patterns.yaml

masking sidecar (masking-service/app/maskers.py)
│ reads MASKING_CONFIG_PATH, compiles regex, redacts [REDACTED_<label>]

Key properties:

  • DRY — shared patterns (email, credit card, IP, JWT, API keys) live once in charts/caip-langfuse/values.yaml under masking.basePatterns.
  • Per region — each values-<region>.yaml adds only its local patterns under masking.regionalPatterns; base patterns are inherited automatically.
  • Fail-safe rendering — a pattern missing label or regex fails helm template/helm upgrade at render time, so a bad values file never ships an empty masking policy.
  • Fallback — if the ConfigMap is not mounted (e.g. local dev), the sidecar falls back to built-in defaults in masking-service/app/maskers.py.

Helm merge semantics (why base + regional are separate)

Helm always uses the chart's values.yaml as the base and overlays -f values-<region>.yaml on top:

  • Maps merge key-by-key — regional files inherit defaults they don't set.
  • Lists are REPLACED entirely — they are not appended.

Because lists replace, we keep shared rules in basePatterns and region rules in regionalPatterns, then concatenate them in the template. If everything lived in a single list, a region file would silently wipe the shared patterns.

Pattern schema

masking:
enabled: true # render the ConfigMap (set false to disable in a region)
configMapName: caip-langfuse-masking-patterns
basePatterns: # defined once in values.yaml
- label: EMAIL # used in the [REDACTED_<label>] placeholder
regex: '...' # Python regular expression
flags: [IGNORECASE] # optional; names from Python `re`
regionalPatterns: # set per env in values-<region>.yaml
- label: CN_ID_CARD
regex: '(?<!\d)\d{17}[\dXx](?!\d)'

Supported flags names (from Python re): IGNORECASE, MULTILINE, DOTALL, VERBOSE, UNICODE, ASCII (and the shorthands I, M, S, X, U, A). Unknown flag names fail closed with an error at sidecar startup.

Fail-closed behavior

The worker's masking callback is controlled by an env var in the region values file:

- name: LANGFUSE_INGESTION_MASKING_CALLBACK_FAIL_CLOSED
value: "true"

fail_closed: true means ingestion is rejected if the masking callback fails — raw PII is never persisted. Use this for compliance-sensitive regions. Set it to false only if availability must be favored over guaranteed redaction.

Reloading patterns after a change

The masking sidecar (volume, mount, and callback env) is already wired once per environment values file, and the ConfigMap name never changes — so a new region or a new pattern requires no sidecar changes.

The sidecar reads /config/patterns.yaml once at startup. A ConfigMap data change does not restart the pod automatically, so after ArgoCD syncs a pattern change, restart the worker so the sidecar reloads:

kubectl rollout restart deploy/<langfuse-worker-deploy> -n <namespace>

Adding a masking rule to an existing region

  1. Edit the region's regionalPatterns in charts/caip-langfuse/values-<region>.yaml.
  2. Open a PR — the Langfuse Helm Validation workflow lints and templates the chart automatically.
  3. Merge to main — ArgoCD syncs the updated ConfigMap.
  4. Restart the worker (see note above) so the sidecar reloads the patterns.

No image rebuild is needed for a pattern-only change.


Example: onboarding a new region (China)

The China team needs to change only their values file. Everything else — the ConfigMap template, the sidecar, the base patterns — is inherited.

Step 1 — Create charts/caip-langfuse/values-cn.yaml

Add a top-level masking: block with China-specific patterns. The shared base patterns (email, credit card, IP, JWT, API keys) come from values.yaml automatically, so only list the local additions here.

# ---------------------------------------------------------------------------
# Server-side masking — CN region-specific patterns
# ---------------------------------------------------------------------------
masking:
enabled: true
regionalPatterns:
# Chinese Resident Identity Card (18 chars, last may be X)
- label: CN_ID_CARD
regex: '(?<!\d)\d{17}[\dXx](?!\d)'
# Chinese mobile phone number
- label: CN_PHONE
regex: '(?<!\d)1[3-9]\d{9}(?!\d)'
# Chinese bank card (UnionPay, 16-19 digits)
- label: CN_BANK_CARD
regex: '(?<!\d)62\d{14,17}(?!\d)'

Step 2 — Validate and roll out

# Render just the ConfigMap for the CN region
helm template charts/caip-langfuse -f charts/caip-langfuse/values-cn.yaml \
--set backups.clickhouse.volumeSnapshots.enabled=false \
--show-only templates/masking-configmap.yaml

Then open a PR (Helm validation runs automatically), merge to trigger ArgoCD, and restart the worker so the sidecar loads the CN patterns.

Verifying it works

Send a value that matches a CN pattern and confirm it is redacted:

kubectl exec deploy/<worker-deploy> -c masking-sidecar -- \
curl -s localhost:8000/mask -H 'Content-Type: application/json' \
-d '{"text":"id 11010119900307721X phone 13800138000"}'
# -> { "text": "id [REDACTED_CN_ID_CARD] phone [REDACTED_CN_PHONE]" }

Validating locally (any region)

# Render just the ConfigMap for a region
helm template charts/caip-langfuse -f charts/caip-langfuse/values-test.yaml \
--set backups.clickhouse.volumeSnapshots.enabled=false \
--show-only templates/masking-configmap.yaml

# Run the masking service tests
cd masking-service
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt pytest httpx
.venv/bin/python -m pytest tests/ -q