You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Threat model: shared log-aggregation tenancy (multiple teams or customers' logs ingested into one ELK / Loki / Splunk index). A reader of logs can correlate user actions across actors and nodes via the propagated trace-id.
Affected files
src/tracing/Tracer.ts:16-21 — JSDoc explicitly documents: "each span's traceId / spanId are merged into the {@link LogContext} scope so log lines stamped during span execution include them automatically".
src/cluster/Protocol.ts:113-119 — EnvelopeMsg.trace carries the raw traceparent string across the wire.
src/internal/ActorCell.ts:608-617 — receives env.trace, opens a span with it as parent; downstream log lines pick up the trace-id via MDC.
src/tracing/RecordingTracer.ts:174-179 — injectContext / extractContext round-trip the raw string.
Caveat — audit framing vs reality
The audit lists "traceparent values written to plain-text debug logs" as a finding. Inspection of the current code shows no log statement that prints env.trace.traceparent verbatim. What does happen is that RecordingTracer-style integration merges traceId/spanId into MDC by design (per the Tracer.ts JSDoc), and every log line emitted during span execution then carries those identifiers — which is the whole point of distributed tracing.
So the framing isn't "we accidentally print a secret in a debug log" — it's "the propagated trace-id is, by design, a stable correlation key, and that's a privacy concern in shared-tenant log indices". The fix is therefore not "stop logging it" but "make it controllable".
This issue tracks the controls: redaction modes, opt-out, and a documented threat-model note. The shape is closer to a feature with safety toggles than a bug-fix.
trace-id is a 128-bit random hex string. Within a single trace, every span has the same trace-id — so anyone with read-access to logs across multiple actors / services can:
Pick a trace-id from a log line: request handled, traceId=4bf92f3577b34da6a3ce929d0e0e4736, …
Search every other service's logs for the same trace-id.
Reconstruct the full request chain: which DB queries ran, which downstream APIs were called, what timings, what errors.
In a single-tenant deployment that's intended behaviour. In a multi-tenant log index (e.g. SaaS provider with multiple customers' logs co-mingled, or a shared corporate ELK with role-based access where the role boundary isn't tight) it becomes a privacy leak:
Customer-A's support engineer with read-access to all logs can see Customer-B's request flow.
Internal abuse: support engineer correlates a senior exec's actions across services.
The framework's responsibility is not to refuse tracing — it's to:
Provide a redaction mode (traceIdInLogs: 'full' | 'hashed' | 'none').
Document the trade-off in the threat-model.
Provide a per-actor opt-out for actors that handle especially sensitive data.
Exploit walkthrough
Step 1 — Multi-tenant log aggregation: company runs a managed-service platform; per-customer actor systems all ship logs to a single Loki cluster. Engineers have cluster-wide read role for triage.
Step 2 — Customer-A's CEO does a sensitive action (acquisition lookup, M&A modelling). The action flows through:
WebFrontendActor.receive (traceId=X)
AuthActor.receive (traceId=X)
BillingActor.receive (traceId=X)
M&AModelActor.receive (traceId=X)
Cross-node call to ExternalAPIActor.receive on node-2 (traceId=X)
Each log line carries { traceId: 'X', actor: '<path>', msg: 'processing' }.
Step 3 — Support engineer (legitimate cluster-wide-read role) is investigating a totally unrelated performance issue. They grep logs for 'M&AModelActor' and find traceId=X.
Step 4 — They pivot:grep traceId=X across the cluster. They see the full request chain — including the action timing, the data classes touched, the downstream APIs. They now know the CEO is running M&A models against company Z.
Step 5 — Information leak. No code was bypassed; no auth check failed. The trace-id was the unintended correlation handle.
Realistic worst case: insider abuse → market manipulation. Less dramatic but more common: shared SaaS log indices where Customer-A's "developer with debug logs read" sees enough of Customer-B's flow to fingerprint their architecture.
How the 8 already-landed security fixes inform this
Idempotency body-fingerprint stored a hash, not the raw body — same shape applies here: traceIdInLogs: 'hashed' mode emits a SHA-256 of traceId + <per-process secret> so trace correlation works within a process restart window but not across cleanly-rotated deployments.
Hello-handshake hijack defence added a privacy-preserving sentinel; the fix wasn't "stop emitting any identity" but "emit a verifiable but not reversible identity". Same principle: emit a bucketed correlation key, not the raw cross-trace-tracker.
The shape: the framework's job isn't to refuse the feature; it's to make the privacy-vs-debuggability trade-off explicit at config time.
Fix design
Track 1 — traceIdInLogs policy on the tracer adapter (primary). Add an option:
exporttypeTraceIdLoggingMode=|'full'// include full hex traceId/spanId in MDC (current default)|'hashed'// SHA-256(traceId + processSecret).slice(0, 16) — still correlates within a process, not across restarts|'short'// first 8 hex chars (collision-prone, lower entropy, harder to fingerprint)|'none';// omit traceId/spanId from MDC entirely (tracing still flows on the wire; just not in log MDC)exportinterfaceRecordingTracerOptions{readonlytraceIdInLogs?: TraceIdLoggingMode;}
hashed mode generates a per-process secret on tracer construction (random 32 bytes); the hashed trace-id is stable within the process lifetime but unrecoverable from logs alone. Cross-process correlation is therefore intentionally broken — that's the privacy property.
Inside that actor's dispatch, the MDC injection skips the trace-id field. Cross-wire propagation continues (so traces still link); only the local log lines omit it.
Track 3 — Wire-format unaffected. The EnvelopeMsg.trace.traceparent field continues to carry the full traceparent — that's required for downstream OTel-aware services to receive a coherent trace. The redaction only affects what lands in log lines via MDC.
Track 4 — Documentation. Add to README "Known security caveats":
- Trace propagation: when tracing is enabled, traceId/spanId are merged
into log MDC by default (per W3C convention). In shared log-indices,
these become correlation handles across tenants. Use
`traceIdInLogs: 'hashed' | 'short' | 'none'` to control or disable.
Wire-level propagation (cross-node tracing) is unaffected.
OtelAdapter gets the same option. The user's OTel SDK is unaffected — it sees full traceparents on the wire.
Backward compatibility
Default unchanged:traceIdInLogs: 'full' is the default (matches W3C convention, what users currently see). Opting in to redaction is a deliberate, documented choice.
No wire-format break: the EnvelopeMsg.trace.traceparent field is unchanged. Cross-node tracing continues to work; downstream OTel-aware services still see full traceparents.
Test plan
Default mode (full) — emit a log line during span execution, verify MDC includes the full 32-char traceId.
hashed mode — same scenario, MDC includes a 16-char hex hash; verify it's stable within process (same traceId → same hash) and different across process restarts (process-secret rotates).
short mode — MDC contains 8 first chars; verify the cross-actor correlation still works for traces within a single process (collision rate is acceptable at 8 hex chars).
none mode — MDC has no traceId field; verify the wire envelope still contains the full traceparent.
Per-actor override — system-default is 'full', but actor X has 'none'; X's log lines have no traceId, sibling actors keep full.
Wire-format test — EnvelopeMsg.trace.traceparent is full-length regardless of traceIdInLogs setting.
Cross-node test — node-1 has 'hashed', node-2 has 'full'; trace flows across the wire; each node's logs honour its own setting.
Severity / Size
Affected files
src/tracing/Tracer.ts:16-21— JSDoc explicitly documents: "each span'straceId/spanIdare merged into the {@link LogContext} scope so log lines stamped during span execution include them automatically".src/cluster/Protocol.ts:113-119—EnvelopeMsg.tracecarries the rawtraceparentstring across the wire.src/internal/ActorCell.ts:608-617— receivesenv.trace, opens a span with it as parent; downstream log lines pick up the trace-id via MDC.src/tracing/RecordingTracer.ts:174-179—injectContext/extractContextround-trip the raw string.Caveat — audit framing vs reality
The audit lists "traceparent values written to plain-text debug logs" as a finding. Inspection of the current code shows no log statement that prints
env.trace.traceparentverbatim. What does happen is thatRecordingTracer-style integration mergestraceId/spanIdinto MDC by design (per theTracer.tsJSDoc), and every log line emitted during span execution then carries those identifiers — which is the whole point of distributed tracing.So the framing isn't "we accidentally print a secret in a debug log" — it's "the propagated trace-id is, by design, a stable correlation key, and that's a privacy concern in shared-tenant log indices". The fix is therefore not "stop logging it" but "make it controllable".
This issue tracks the controls: redaction modes, opt-out, and a documented threat-model note. The shape is closer to a feature with safety toggles than a bug-fix.
Background
The W3C
traceparentformat is:trace-idis a 128-bit random hex string. Within a single trace, every span has the sametrace-id— so anyone with read-access to logs across multiple actors / services can:trace-idfrom a log line:request handled, traceId=4bf92f3577b34da6a3ce929d0e0e4736, …trace-id.In a single-tenant deployment that's intended behaviour. In a multi-tenant log index (e.g. SaaS provider with multiple customers' logs co-mingled, or a shared corporate ELK with role-based access where the role boundary isn't tight) it becomes a privacy leak:
The framework's responsibility is not to refuse tracing — it's to:
traceIdInLogs: 'full' | 'hashed' | 'none').Exploit walkthrough
Step 1 — Multi-tenant log aggregation: company runs a managed-service platform; per-customer actor systems all ship logs to a single Loki cluster. Engineers have
cluster-wideread role for triage.Step 2 — Customer-A's CEO does a sensitive action (acquisition lookup, M&A modelling). The action flows through:
WebFrontendActor.receive(traceId=X)AuthActor.receive(traceId=X)BillingActor.receive(traceId=X)M&AModelActor.receive(traceId=X)ExternalAPIActor.receiveon node-2 (traceId=X)Each log line carries
{ traceId: 'X', actor: '<path>', msg: 'processing' }.Step 3 — Support engineer (legitimate cluster-wide-read role) is investigating a totally unrelated performance issue. They grep logs for
'M&AModelActor'and find traceId=X.Step 4 — They pivot:
grep traceId=Xacross the cluster. They see the full request chain — including the action timing, the data classes touched, the downstream APIs. They now know the CEO is running M&A models against company Z.Step 5 — Information leak. No code was bypassed; no auth check failed. The trace-id was the unintended correlation handle.
Realistic worst case: insider abuse → market manipulation. Less dramatic but more common: shared SaaS log indices where Customer-A's "developer with debug logs read" sees enough of Customer-B's flow to fingerprint their architecture.
How the 8 already-landed security fixes inform this
traceIdInLogs: 'hashed'mode emits a SHA-256 oftraceId + <per-process secret>so trace correlation works within a process restart window but not across cleanly-rotated deployments.The shape: the framework's job isn't to refuse the feature; it's to make the privacy-vs-debuggability trade-off explicit at config time.
Fix design
Track 1 —
traceIdInLogspolicy on the tracer adapter (primary). Add an option:hashedmode generates a per-process secret on tracer construction (random 32 bytes); the hashed trace-id is stable within the process lifetime but unrecoverable from logs alone. Cross-process correlation is therefore intentionally broken — that's the privacy property.Track 2 — Per-actor opt-out via
props.config.traceIdInLogs. Actors handling especially sensitive data (PII, financial, M&A) can override:Inside that actor's dispatch, the MDC injection skips the trace-id field. Cross-wire propagation continues (so traces still link); only the local log lines omit it.
Track 3 — Wire-format unaffected. The
EnvelopeMsg.trace.traceparentfield continues to carry the full traceparent — that's required for downstream OTel-aware services to receive a coherent trace. The redaction only affects what lands in log lines via MDC.Track 4 — Documentation. Add to README "Known security caveats":
API surface
OtelAdaptergets the same option. The user's OTel SDK is unaffected — it sees full traceparents on the wire.Backward compatibility
Default unchanged:
traceIdInLogs: 'full'is the default (matches W3C convention, what users currently see). Opting in to redaction is a deliberate, documented choice.No wire-format break: the
EnvelopeMsg.trace.traceparentfield is unchanged. Cross-node tracing continues to work; downstream OTel-aware services still see full traceparents.Test plan
hashedmode — same scenario, MDC includes a 16-char hex hash; verify it's stable within process (same traceId → same hash) and different across process restarts (process-secret rotates).shortmode — MDC contains 8 first chars; verify the cross-actor correlation still works for traces within a single process (collision rate is acceptable at 8 hex chars).nonemode — MDC has no traceId field; verify the wire envelope still contains the full traceparent.'full', but actor X has'none'; X's log lines have no traceId, sibling actors keepfull.EnvelopeMsg.trace.traceparentis full-length regardless oftraceIdInLogssetting.'hashed', node-2 has'full'; trace flows across the wire; each node's logs honour its own setting.Acceptance criteria
RecordingTraceracceptstraceIdInLogs: 'full' | 'hashed' | 'short' | 'none'(default'full').OtelAdapteraccepts the same option with the same semantics.Props.withTraceIdInLogs(...).EnvelopeMsg.trace.traceparent) is unaffected.