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)
custom_hookruns 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.yamlundermasking.basePatterns. - Per region — each
values-<region>.yamladds only its local patterns undermasking.regionalPatterns; base patterns are inherited automatically. - Fail-safe rendering — a pattern missing
labelorregexfailshelm template/helm upgradeat 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
- Edit the region's
regionalPatternsincharts/caip-langfuse/values-<region>.yaml. - Open a PR — the
Langfuse Helm Validationworkflow lints and templates the chart automatically. - Merge to
main— ArgoCD syncs the updated ConfigMap. - 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