Skip to content

feat(api): trusted-proxy-aware client IP in traces & logs (+ gate trace-context propagation) #333

Description

@EricAndrechek

Area: api · observability — edge integration · split out of #332 / #281

Context

PR #332 dropped chi's deprecated middleware.RealIP (#281) because it rewrote r.RemoteAddr from 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 read r.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's client.address) is the proxy's IP. We want the real client IP back, captured safely, plus to formalize trace continuation from the proxy.

Scope

  1. Trusted-proxy config. Declare trusted proxies — a CIDR list (e.g. 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).
  2. Client-IP extraction (trusted-proxy-aware). When the immediate peer is a trusted proxy, walk X-Forwarded-For right-to-left and take the first hop that is not itself a trusted proxy — the real client. ⚠️ Do not take the leftmost / True-Client-IP value blindly: that's client-controlled and would reintroduce the exact spoofing the RealIP drop just fixed.
  3. Surface it. Attach the resolved client IP as a span attribute (client.address) and a structured-log field, alongside the existing trace_id/span_id (internal/observability/logger.go).
  4. Trace-context propagation (already works — formalize + gate). WaveHouse already sets the W3C TraceContext propagator (internal/observability/provider.go:110) and uses otelhttp without WithPublicEndpoint, so an incoming traceparent from an nginx/Caddy OTel module is already adopted as the parent — the request joins that trace and logs already carry the continued trace_id. Two gaps: (a) it's undocumented and has no test for the incoming-traceparent case; (b) it trusts any client's traceparent (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).
  5. Docs. Add a "trusted proxy / client IP" section to the reverse-proxy page, and document that a proxy-supplied traceparent is 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/apiHTTP handlers, routing, middlewarearea/observabilityMetrics, logs, traces, health, profilingdocumentationImprovements or additions to documentationenhancementNew feature or requestsecuritySecurity-sensitive issue or fix

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions