Skip to content

feat: add privacy-safe scan telemetry - #45

Merged
LKSNDRTMLKV merged 4 commits into
mainfrom
feat/scan-telemetry
Jul 27, 2026
Merged

feat: add privacy-safe scan telemetry#45
LKSNDRTMLKV merged 4 commits into
mainfrom
feat/scan-telemetry

Conversation

@LKSNDRTMLKV

Copy link
Copy Markdown
Member

Summary

Adds privacy-safe scan telemetry: aggregate counts of how often each passport is resolved, surfaced to the operator — without recording anything about the person who scanned.

The public resolver has no database (it is kept stateless for an edge target), so it counts resolutions in memory and periodically flushes aggregates to the node's internal, mTLS-gated ingest endpoint. The node persists them and the operator reads them back through the API, the CLI, and the interactive console.

Design

resolver (in-memory counts) --mTLS flush--> node POST /vault/internal/scan-batch
                                                    │ upsert
                                                    ▼
                                    odal.scan_telemetry / odal.qr_render
                                                    │
                              GET /vault/api/v1/dpp/{id}/stats  ·  GET /vault/api/v1/stats
                                                    │
                                  odal passport stats <id>  ·  odal stats  ·  console
  • Storage (ops/pg/0025_scan_telemetry.sql): two upserted counter tables keyed only by (passport, day, surface). There is no column for an IP, user agent, session, or per-event row — the schema itself is the privacy policy. A CHECK (variant IN ('html','json')) structurally prevents a QR render being written as a resolution; retention is a rolling 24-month prune (the app role gets a scoped DELETE).
  • Ingest (POST /vault/internal/scan-batch): mounted off both the public and Bearer trees, gated by client-certificate mTLS (CN=odal-resolver). The mTLS middleware was promoted from the identity service into dpp-common::mtls and parameterised by allowed CN, so there is one audited implementation (identity is now a thin shim).
  • Resolver: in-memory accumulator + flush task; counts terminal-view resolutions only (/dpp/{id} html/json), with QR-image renders tracked as a separate metric and the GS1 /01/{gtin} redirect deliberately not counted (its followed terminal view is). Off entirely unless SCAN_INGEST_URL is configured.
  • Read surfaces: GET …/dpp/{id}/stats + GET …/stats; odal stats / odal passport stats <id> (--days, --json); and an interactive "Scan telemetry" entry in the console (operator rollup + per-passport).

Key decisions

  • Terminal-view counting only — one physical scan counts once (no GS1 redirect double-count).
  • QR renders are a separate metric, never summed into resolutions — a render is label production, not a scan.
  • Schema is the policy — nothing about the scanner is representable, so nothing can leak.
  • mTLS from day one for the ingest boundary — an unauthenticated writer could otherwise inflate an operator's counts.
  • Bounded retention (24 months) — aggregates do not accumulate forever.

Testing

  • Unit + component: wire types, mTLS enforcement, ingest parsing/fan-out, window clamping, and the resolver→HTTP flush (real localhost server, incl. retain-on-5xx).
  • resolver_e2e: the counting model end to end — GS1 redirect not double-counted, QR not counted as a scan.
  • scan_ingest_route: the internal endpoint rejects a caller with no client certificate.
  • Postgres integration (just test-pg, real postgres:17): migration applies, upsert/stats/prune and the variant CHECK all pass.
  • Verified live end to end against a real node + resolver + Postgres: published a passport, resolved 2×json + 3×html + 1×qr + 1×GS1-redirect → counts came back html=3, json=2 (5 resolutions), qrRenders=1, GS1 redirect uncounted, read back via odal stats.

Full just check is green.

Out of scope (separate repos / branches)

  • dpp-infra: the Caddy client_auth + on-VM cert provisioning that terminates the mTLS in production (locally the flush uses MTLS_ALLOW_INSECURE=true).
  • dpp-web: the public trust-page disclosure of what a scan count is and is not.
  • dpp-docs: the data-protection assessment.

Reviewer notes

  • New env: resolver SCAN_INGEST_URL / SCAN_FLUSH_INTERVAL_SECS / SCAN_FLUSH_CLIENT_IDENTITY; node reuses identity's MTLS_*. Documented in .env.example.
  • New migration 0025 is additive; the app role's new DELETE grant is scoped to the two telemetry tables only.

@codacy-production

codacy-production Bot commented Jul 24, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 6 high · 3 medium

Alerts:
⚠ 9 issues (≤ 0 issues of at least minor severity)

Results:
9 new issues

Category Results
Compatibility 1 medium
Security 6 high
Performance 2 medium

View in Codacy

🟢 Metrics 88 complexity · 12 duplication

Metric Results
Complexity 88
Duplication 12

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements the requested privacy-safe scan telemetry; however, it is currently not up to standards due to critical security and stability risks.

Major issues that must be addressed before merging include:

  • A security vulnerability in the mTLS middleware where the authentication fails open if the proxy shared secret is missing, allowing for header-injection attacks.
  • An unbounded memory growth risk in the resolver's ScanCounter, which could lead to OOM crashes under high load or targeted attacks.
  • Logic flaws in telemetry retry handling that create 'poison batches,' potentially blocking all subsequent telemetry flushes if a single malformed batch is rejected by the vault with a 4xx error.
  • RFC 4514 non-compliance in the Distinguished Name (DN) parser, which will cause legitimate certificates with escaped commas to be rejected.

About this PR

  • The telemetry implementation introduces several systemic risks to the resolver's availability. Specifically, the combination of unbounded in-memory maps and 'poison batch' retry logic creates a scenario where the service could easily crash or cease telemetry reporting during period of high traffic or upstream vault instability.

Test suggestions

  • Verify the internal scan-ingest endpoint rejects requests without a valid client certificate.
  • Verify that GS1 redirects (/01/{gtin}) do not increment the scan counter.
  • Verify that QR image renders increment a separate bucket and are not summed into scans.
  • Verify the database 'variant' CHECK constraint prevents invalid resolution types (e.g., 'qr') from entering the scan table.
  • Verify the resolver's merge-back logic correctly retains counts if a flush to the node fails.
  • Verify the daily retention prune task removes data older than the 730-day horizon.

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment thread crates/dpp-resolver/src/infra/scan_counter.rs
Comment thread crates/dpp-common/src/mtls.rs
Comment thread crates/dpp-resolver/src/infra/scan_counter.rs
Comment thread crates/dpp-common/src/mtls.rs
Comment thread crates/dpp-dal/src/pg/repo_scan.rs
# Conflicts:
#	crates/dpp-dal/tests/pg_integration.rs
#	crates/dpp-node/src/boot/tasks.rs
@LKSNDRTMLKV
LKSNDRTMLKV merged commit ce5b388 into main Jul 27, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant