Repository navigation
feat(api): trusted-proxy-aware client IP in traces & logs (+ gate trace-context propagation) #333
Description
Activity
- addedenhancementNew feature or requestNew feature or requestarea/observabilityMetrics, logs, traces, health, profilingMetrics, logs, traces, health, profilingarea/apiHTTP handlers, routing, middlewareHTTP handlers, routing, middleware
on Jun 10, 2026 - addedsecuritySecurity-sensitive issue or fixSecurity-sensitive issue or fix
on Jun 10, 2026 coderabbitai commented
on Jun 10, 2026 coderabbitaiboton Jun 10, 2026 – with coderabbitaiMore actions🔗 Related PRs
#123 - fix(api): drop CORS credentials + skip same-origin decoration [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.- Create Plan
🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:issue_enrichment: auto_enrich: enabled: false
💬 Have feedback or questions? Drop into our discord!
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Jun 10, 2026 - added a commit that references this issue
on Jun 10, 2026 - added 2 commits that reference this issue
on Jul 6, 2026 Folding in a related item from #378 (the operator-key PR): the operator-key audit log (
internal/auth/auth.go) wants a request-scoped correlation ID (chi'srequest_id). I intentionally did not stamp it per-call-site there — it belongs here, in the globalTraceHandler(internal/observability/logger.go), alongside the trusted-proxy client IP this issue already covers.Concretely, to add here:
TraceHandler.Handleshould also pullrequest_idfrom context (chi'smiddleware.GetReqID, always populated via themiddleware.RequestIDatrouter.go) and stamp it on every record, next totrace_id/span_id.- The handler should wrap the base handler in all modes, not just when OTel logs are enabled. Today it's only installed in the OTel-logs path (
main.go); the plainslog.JSONHandlerfallback gets no context-derived fields, so a non-OTel deployment currently has no correlation ID at all.
Net once this lands: every log line — including the operator-key audit line — uniformly carries
request_id(OTel-independent) and the trusted-proxy client IP, wired once instead of per call site.— Claude Code
The request-id item folded in from #378 (stamp chi's
request_idfrom context inTraceHandler, next totrace_id/span_id) is now tracked in #771, together with the resolved tenant, so it can ship without the trusted-proxy design. This issue keeps the trusted-proxy client IP and the trace-context gating. Both are part of the observability epic #777.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsBacklog
Area: api · observability — edge integration · split out of #332 / #281
Context
PR #332 dropped chi's deprecated
middleware.RealIP(#281) because it rewroter.RemoteAddrfrom spoofable forwarded headers (X-Forwarded-For/X-Real-IP/True-Client-IP) on every request, whether or not a trusted proxy set them, and nothing in WaveHouse readr.RemoteAddr(no per-IP logic — rate limiting is the reverse proxy's job). That removed the IP-spoofing vector (GHSA-3fxj-6jh8-hvhx / GHSA-rjr7-jggh-pgcp / GHSA-9g5q-2w5x-hmxf) and unblocked the go-deps dependabot bump (#209).The trade-off: WaveHouse now has no real client IP in its own traces/logs — behind a proxy,
r.RemoteAddr(and OTel'sclient.address) is the proxy's IP. We want the real client IP back, captured safely, plus to formalize trace continuation from the proxy.Scope
WH_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12) is the correct model. Default empty ⇒ trust nothing (use the immediate peer).X-Forwarded-Forright-to-left and take the first hop that is not itself a trusted proxy — the real client.True-Client-IPvalue blindly: that's client-controlled and would reintroduce the exact spoofing the RealIP drop just fixed.client.address) and a structured-log field, alongside the existingtrace_id/span_id(internal/observability/logger.go).internal/observability/provider.go:110) and usesotelhttpwithoutWithPublicEndpoint, so an incomingtraceparentfrom an nginx/Caddy OTel module is already adopted as the parent — the request joins that trace and logs already carry the continuedtrace_id. Two gaps: (a) it's undocumented and has no test for the incoming-traceparentcase; (b) it trusts any client'straceparent(trace pollution / a direct client injecting itself into your traces). Gate continuation on the same trusted-proxy boundary (e.g.otelhttp.WithPublicEndpoint()or a custom check, so an untrusted peer's incoming trace is linked, not adopted).traceparentis continued.Security note
The XFF-parsing logic is a classic footgun — getting the trusted-hop walk wrong rebuilds a spoofable-IP feature. Warrants careful review + tests: spoof attempts, multiple chained proxies, IPv6, and missing/empty headers.
Related: #281 (dropped RealIP), #332 (the PR that dropped it), #241 (reverse-proxy docs), #209 (go-deps dependabot, unblocked by the drop).