LinkMesh
LinkMesh Observability Data Collection Management
Compliance Governance

PII vs CID

Detection alone can't find what only your own client data defines.

linkmesh.io
Philippe Braxmeier Philippe Braxmeier ← Back to blog
10 min read

An employee’s name is personal data. A corporate client’s name is not — and a scanner tuned for one walks straight past the other.

That’s not a technicality. PII and CID sit in different legal boxes, they’re triggered by different facts, and a live scanner — regex, allow-list, even an LLM — cannot fully close the second box. Not because the engineering is immature. Because the thing it would need to check against isn’t a pattern. This post is about why that gap exists, what the published detection numbers actually say about it, and the one architectural move that makes the gap stop mattering.

PII — personal data under GDPR or Switzerland’s revised FADP (revDSG) — is triggered by whose data it is: a natural person. An employee’s name, a private customer’s email, a home address. CID — client-identifying data under Swiss banking secrecy (Art. 47 BankG) and FINMA Circular 2023/1 — is triggered by whose relationship it is: a bank’s or insurer’s client, whether that client is a natural person or a company.

The two categories overlap in places and diverge in ways that matter. A corporate client’s name is CID, not PII — no natural person, no GDPR trigger, full banking-secrecy exposure. An employee’s name is PII, not CID — no client relationship, but every data-protection obligation applies. Run a scanner tuned for PII and you protect your own staff while every corporate client record sails through unflagged. Run one tuned for CID and the reverse happens. Checking one box and assuming it covers the other is the single most common gap in this space, and it’s invisible until someone asks which box a specific field falls into.

FINMA’s circular doesn’t treat CID as one flat category either. As summarised by BigID, it groups identifiers into three tiers by how directly they identify a client: direct identifiers like a name, address or signature; indirect identifiers such as an account, contract or policy number, which only identify someone once linked to the entity that issued them; and data that’s identifying only in combination — a postal code, a birth year, a relationship type — none of it sensitive alone, all of it sensitive together. That third tier is where most detection strategies quietly stop working, because there’s no single field to flag.

Why pattern matching can’t find CID

A regex or an allow-list can catch a Swiss social security number or an IBAN — those have a fixed format and a check digit. Customer numbers, account numbers, policy numbers, contract numbers, claim numbers, end-customer user IDs — the actual bulk of CID moving through a telemetry pipeline — have neither. Every organisation runs its own numbering scheme. A generic rule for “6–12 digit sequence” doesn’t find customer numbers with any more precision than it finds ticket numbers, error codes, and port numbers, and widening the pattern just trades false negatives for a flood of false positives.

The DLP industry’s standard answer to this is Exact Data Match (EDM): load the real customer master data into the filtering layer and match against it directly instead of guessing at a pattern. It works — and it fails completely for anyone building shared infrastructure for multiple clients. To match exactly, an EDM engine needs the customer identifier space it’s protecting loaded into the very system doing the filtering. For a platform or pipeline vendor, that means centralizing precisely the data the whole exercise exists to keep decentralized. The fix would recreate the exposure it prevents.

What the detection numbers actually say

It’s tempting to assume a large language model closes this gap where regex can’t. The published benchmarks say: some, not enough. A 2026 evaluation of hybrid multilingual PII detection across 13 locales measured a classical fine-tuned NER model at 0.36 weighted F1, a zero-shot LLM with no task-specific preparation at 0.56, and a hybrid of deterministic rules plus a context-aware LLM at 0.66 — the best result in the study, and still a third of matches wrong in some form. In a narrower, more favourable setting — English-language financial documents, structured enough for both a regex layer and a trained model to do well — a hybrid rule-based-NLP-plus-ML approach reached 94.7% precision and 89.4% recall on a synthetic benchmark, dropping to roughly 93% accuracy on real audit reports and vendor bills. That’s a genuinely strong result for a curated document type — and it still means roughly one in ten true positives goes unflagged, running at billions of log lines a day rather than a batch of documents.

There’s a second problem underneath the accuracy numbers, and it doesn’t show up in any F1 score: an external AI service used to detect PII is itself a disclosure. Sending log content to a third-party model to check whether it contains data that must never leave your infrastructure means it already did — to the detector. The tool built to prevent the exposure creates the exposure it’s supposed to catch, unless the model runs entirely inside the boundary you’re trying to protect. Whatever precision number a vendor quotes, check where the inference actually happens before it changes anything.

The blind spot specific to observability

Everything above holds for any log pipeline. Observability data has a structural wrinkle most write-ups about PII in logs don’t mention: the most common carriers of CID aren’t in the log body at all — they’re in the resource metadata. A hostname carrying a client abbreviation (edge-acme-01), a Kubernetes namespace named after a tenant, a certificate common name — none of it looks like “sensitive data” by the usual definition, because content-scanning tools are built to inspect message bodies, and this is metadata about the message. Masking PII in logs covers the redaction patterns that work well on structured, named fields; this is the layer above that, where the field names themselves are the leak.

Free-text containers make it worse in the other direction: stack traces, a raw SQL statement with literal WHERE clause values, a full request or response body. Nothing about their shape tells a parser where the sensitive part starts or ends, so any content-based method is back to brittle regex over unstructured text. And base64-encoded payloads — increasingly common as services pass structured blobs through generic transport fields — are opaque to every content-based method at once, pattern-based or model-based. There’s nothing to match against; it’s binary until decoded, and nothing in a telemetry pipeline decodes it before shipping it onward.

None of this is new in kind. Latanya Sweeney’s foundational re-identification study found that ZIP code, gender and date of birth alone uniquely identify 87% of the US population — three fields, none of them sensitive in isolation, fully identifying in combination. The same logic applies to resource attributes: a region, an environment tag, and a truncated hostname fragment can jointly isolate a single tenant even when each one individually looks like ordinary infrastructure metadata. Detection tuned to flag values misses identification that only exists as a combination.

The fix that already exists in the OpenTelemetry ecosystem is trace and correlation IDs. A request-scoped, opaque identifier lets you correlate telemetry across services — group by session, follow a request end to end — without ever carrying a customer number to do it. That’s not a workaround; it’s the correct replacement for putting a real identifier in a log line, and it’s already standard practice in most instrumented systems. The gap isn’t the technique. It’s the resource labels and free-text fields that never went through it.

The standards already say this

None of the standards this space usually cites claim a scanner can catch everything, and read together they converge on the same answer: don’t put the raw identifier there to begin with. The OWASP Logging Cheat Sheet recommends logging a salted hash of a session identifier instead of the identifier itself, precisely so correlation survives without the raw value ever landing in a log. PCI DSS Requirement 3.4 requires a card number to be unreadable everywhere it’s stored, logs included — truncation, hashing, or tokenization, never the value as written. And NIST SP 800-122 draws its own version of FINMA’s tier structure independently: linked information is already associated with an individual, linkable information merely could be, given the right combination. Three different bodies, three different domains, the same conclusion — treat the boundary as the control, not the inspection.

What this means for the pipeline

If detection is structurally incomplete — and 0.66 weighted F1 under generous, well-funded evaluation conditions says it is — then a compliance strategy that depends on catching every violation at the edge is one that fails quietly. Nobody notices a missed regex match. Someone notices during an audit, or during an incident, by which point the data already crossed the boundary.

The alternative isn’t a better scanner. It’s not needing the scanner to be right, because the decision was already made upstream. That means classifying what a data stream is allowed to carry when a collector is onboarded, not re-deciding it on every message that flows through — and making the routing decision before the network boundary, not after.

Fleet-wide routing topology: collectors on the left, connected through classification-based routes to separate destinations on the right, including a dedicated compliance archive

Fleet-wide routes, split by classification, to separate destinations — including one that never leaves the perimeter.

This is what LinkMesh’s routing layer is for, and it’s worth being precise about what it actually does today rather than what would be nice. Every route carries a resource-attribute matcher — built through a guided field/operator/value form, with raw OTTL available as the Advanced fallback for expressions the builder doesn’t cover — and label-based destination selection, so tagging a destination lets it auto-join matching routes. Tag a stream’s origin once at onboarding (which collector, which namespace, which client boundary it belongs to), and every record from it evaluates against that classification before it reaches a destination, self-hosted or otherwise. Worth naming the edge honestly, because a post arguing for architecture over detection should hold itself to the same standard: the destination selector is equality-only today — no In, NotIn or existence operators — so a selector that needs anything more expressive than an exact match is a custom, API-authored one rather than something drawn on the canvas. That’s a real limit, not a hidden one.

Routing only matters if the control plane making the decision is somewhere you trust. That’s the other half of this: LinkMesh runs as a single, zero-dependency binary with embedded storage, so the classification and routing step itself runs on infrastructure you hold, not a hop through someone else’s cloud. The adjacent problem — credentials rather than client data — gets the same treatment: secrets live in a built-in vault and are rendered only at delivery, never written into a stored config or committed to Git. Different data, same principle: don’t build a control that depends on something leaving first.

None of this replaces field-level masking — it still belongs on whatever leaves the boundary intentionally, and Masking PII in Logs covers the redaction patterns for that layer. The difference is what each layer is trusted to do. Masking is the right tool for data you’ve already decided can leave. It was never going to be the tool that makes that decision for you — no scanner is, and now there are published numbers that say so. Classify at the source, route before the boundary, and the question a detector keeps failing to answer stops needing to be asked at the edge.

See Trust for the full data-handling model, or set up a route on your own infrastructure at linkmesh.io/install.