Description
A signal-label sweep across a real estate found an agent CLI exporting a per-person email address as a METRIC LABEL. That is two defects in one series: personal identity in a metrics store that has no access model for it, and an unbounded-cardinality label that grows with headcount. Neither is visible anywhere in this platform today, and both are exactly the kind of finding the risk surface exists to raise.
The signal-label inventory added for Pillar K already reads label NAMES per stack. Detecting these patterns is therefore nearly free - it is a classification pass over data the collector now holds, not a new read.
What to detect
Identity-bearing label NAMES. Match the label key, never the value, against a versioned pattern set. Candidates, all generic and none derived from any one estate:
email, e_mail, mail, user_email, useremail, owner_email
user, username, user_name, user_id, userid, login, account, account_name
person, employee, employee_id, staff_id, upn, principal, subject
full_name, first_name, last_name, given_name, surname, display_name
phone, mobile, msisdn, telephone
ip, ip_address, client_ip, remote_addr, source_ip, x_forwarded_for
session, session_id, cookie, token, api_key, authorization, secret, password
patient, patient_id, mrn, nhs_number, ssn, national_id, dob, date_of_birth
customer_email, customer_name, tenant_email
Unbounded-cardinality label names, a separate and overlapping class: uuid, guid, trace_id, span_id, request_id, correlation_id, run_id, job_id, build_id, pod_template_hash, container_id, task_id, pid, timestamp, epoch, url, path, query, full_path, endpoint where unparameterised.
Absolute rules
- Match on the label KEY only. Never read, store, log, emit or sample a label VALUE for this purpose. The whole point is to report that identity-shaped data exists, not to collect it. A finding that quotes an example value has become the leak it was reporting.
- Never emit a label name as a metric label. Label names are unbounded. The finding is a per-stack COUNT plus a per-class enum; the offending label names go to a
views/table and Loki, exactly as metric names already do. - Publish a confidence tier.
emailas a label key is near-certain;accountandsubjectare frequently legitimate infrastructure labels. Report high-confidence and possible separately and never merge them into one number an operator cannot defend. - This is a report, never a remediation. The platform must not propose dropping a label, and must not touch an Adaptive Metrics rule to do it. Surfacing is the deliverable.
- The pattern set is a versioned data file with a test, like the technology registry, and it must contain nothing derived from a specific customer estate. Generic patterns only. A test asserts no pattern is a bare single letter and that the set carries no identifier resembling a real organisation, person or stack.
Where it lands
Pillar E, risk and hygiene, alongside the existing public-dashboard and access-policy findings. It is the same shape: an inventory the org almost certainly cannot see, surfaced with its measured-stack denominator so an unreadable stack is never a clean zero.
New output:
views/risk_label_hygiene- stack, label name, class, confidence tier, which signal it was seen on.- Bounded metrics: findings per class and per confidence tier, plus stacks measured as the denominator. No label name and no stack-by-label cross product in Mimir.
- A risk dashboard row leading with the high-confidence identity count and the measured-stack denominator beside it.
Why it is worth the space
An estate owner cannot grep their own label space at this scale, and the two consequences land on different teams: the privacy exposure is a compliance conversation and the cardinality is a cost conversation. One inventory serves both. It also strengthens the coverage surface indirectly, because a label carrying per-person identity is a strong hint that a service identity is machine-generated rather than a real application.
Acceptance Criteria
- #1 Daily label-name and label-value reads detect identity-bearing patterns per signal and publish raw matched values in a bounded risk view under the owner-approved deployment privacy policy
- #2 Label names and values never become metric labels; matched detail is view-only
- #3 High-confidence and possible findings are reported separately
- #4 Pattern set is versioned, tested, generic and contains nothing estate-specific
- #5 Findings carry measured-stack coverage and sampled/matched counts; unreadable inputs are absent, never clean zeros
- #6 The platform reports only and never proposes or performs remediation
Definition of Done
- #1 python3 -m pytest tests -q
- #2 tofu fmt -check -recursive terraform; tofu init -backend=false and tofu validate pass for terraform/ and terraform/examples/standalone/
- #3 customer-identifier and shipped-text gates from .github/workflows/ci.yml return clean
Implementation Plan
Implement the versioned generic label-name pattern data and pure classifier with focused tests, then integrate its bounded metrics, view, dashboard and hydration wiring before security review.
loop3 D1b then I-0018: prove all-signal label names/value routes, window, limits and payload bounds; implement daily bounded raw-match risk view under D2, no value/name metric labels; coverage, fixtures, dashboard, gate/review/CI, dev proof.
Implementation Notes
Wave 1 source-contract audit found no generic label-name payload in collector/sources/signal_inventory.py. The source currently exposes metric names plus selected label values used for service and technology matching. Implementing the classifier now would either inspect values contrary to the privacy contract or invent data the collector does not gather. Resume after deciding and implementing a key-only, all-signal label-name source contract with explicit minimisation and retention behavior.
CORRECTION, 2026-09-11 wave 1 closeout. The wave 1 audit finding above is WRONG as stated. It checked only collector/sources/signal_inventory.py. A key-only label-name payload DOES already exist: collector/sources/dataplane.py cardinality() calls Mimir /api/prom/api/v1/cardinality/label_names and returns top_labels, a list of the highest-cardinality label NAMES per stack with their value counts and no values. It already reaches the scan envelope, Loki and S3, and that file’s own comment records the privacy decision for it. collector/pillars/cost.py cost_cardinality_outliers publishes only top_labels[0] as Worst label and discards the other nineteen names.
What is genuinely missing is narrower than the note claimed: the payload is Mimir only, and it is the top N by cardinality rather than a complete label-name set. That is a good input for the unbounded-cardinality class and a poor one for the PII class, because a low-cardinality identity label such as an owner slug never enters the top N.
This task is therefore split. The unbounded-cardinality half is commissioned as GCI-0018.01 over the existing payload with no new source contract. This parent stays Parked for the identity-bearing half only, and resumes after an all-signal key-only label-name source contract exists across Mimir, Loki, Tempo and Pyroscope.
loop3 admitted 2026-09-30 under owner goal: root owns tracker; bounded lanes own implementation/discovery, evidence pending. No acceptance claimed yet.
Owner override 2026-09-30 goal D2 replaces the historical key-only restriction: daily reads may inspect label names AND values and publish raw matched values in full, capped at a stated per-stack/label/class bound. Values remain forbidden as metric labels. Historical description is retained as provenance, not the active privacy contract. No AC is claimed yet.
Loop3 deployment hold: new independent privacy prerequisite review found latest raw-match view has no current-object expiry; scan90/noncurrent7 do not bound stale current view. D2 permits full matched values but not indefinite retention. Targeted repaira2 admitted: lifecycle exact full raw-view filename prefix only, reuse positive whole scan-retention90 defaults, preserve other last-good views, scans/noncurrent7/IAM. Policy is eligibility since last publication plus version expiry/async processing, not strict observation-age erasure. No collector execution on new source before actual controls pass. Customer rollout requires a bucket-owner equivalent rule; lifecycle writes exceed current customer write fence, so that prerequisite remains a run-end authorization question rather than implicit Terraform drift.
Loop3 dev source6ebedd3dccc59ad1bf43a9a7a6cc421abe4b66bc/imagebc024b60cce3917441e679f7b9a423ce3199d978a01923d26ce7b15dc4a2f4f4 exercised on actual dailyT2 and hydratedT1/T3: riskview18 rows, coverage20 rows; full raw matches remained approved S3-only, no raw values printed/persisted in proof. Actual dedicated full-filename currentexpiry90/scans90/globalversionexpiry7/versioningEnabled/AES256/publicblocking/TLSdeny and unchanged view/task IAM verified before publication. Independent source retention repaira2 PASS, fullgate1649passed2existing skips7971subtests/CR0all6/CI36745573818success; both sourceattempts consumed. Five readerNone/IDs/tokens/SSMversions/fullpairsets identical pre/post provisioner. Dashboard risk/raw coverage refs GET-confirmed. Customer publication not exercised: existing policy lacks current-view expiry and bucket write exceeds owner fence; retain run-end authority question. Local source fences/required regression gates passed, actual privacy readbacks separate.
Root counter correction: prior state already recorded original collection source attempts a1 and a2. The later new raw-view retention repair was incorrectly briefed/returned as a2; cumulative task/criterion accounting is a3, with a4 unused within the goal four-attempt ceiling. Root rescoped/delegated this repair to complex-worker-push rather than treating it as a fresh worker counter; this labelling/routing deviation is disclosed, not a counter reset. Source repair/independent review proof remains exact6eb. No implementation was admitted after18:30.
Final Summary
No product implementation was accepted. A draft classifier test was archived under codex/backups and excluded from the product tree. The task is parked at the missing key-only label-name source contract; no acceptance criterion is claimed.
Daily bounded four-signal source and generic versioned classifier shipped; actual dev raw-match18-row S3 view and20-row coverage plus rendered/read-back dashboard prove public boundary. Enforced exactrawview90/scans90/noncurrent7/Enabledversioning/AES256/publicblock/TLS/unchangedIAM proved beforedailyT2; existingfiveNone readers unchanged IDs/SSM/pairs. No new businessseries or metric identities, no rawmatch outsideapprovedS3 in proof. Finalintegratedgate1649pass2skips7971subtests, rootexact7998CI36768983974success; source6ebCR0/SecPASS. Sourceattempt cumulative3(notfresha2), fourthunused; route/accountingdeviation disclosed. Task doneondev/source; campaigncustomerrollout remainsblockedpermissionfence, notclaimed.