Skip to content

fix(http): serve /status/diagnostics only on the internal interface - #4442

Open
stevenvegt wants to merge 2 commits into
masterfrom
fix/diagnostics-internal-only
Open

fix(http): serve /status/diagnostics only on the internal interface#4442
stevenvegt wants to merge 2 commits into
masterfrom
fix/diagnostics-internal-only

Conversation

@stevenvegt

Copy link
Copy Markdown
Member

/status/diagnostics is now refused with 403 on any interface other than the internal one. The endpoint discloses the exact software version and git commit (which answers "is this node patched" for version-scoped advisories), the gRPC peer list including remote addresses, node DIDs and authentication state, and DID document/credential store counts. The deployment documentation has always listed it under internal endpoints, but it was actually served publicly as well.

Why middleware instead of binding the route internally: interface binding (MultiEcho) resolves on the first path segment only and rejects subpath binds, so /status and /status/diagnostics cannot be bound to different interfaces through the bind table; the route is registered on every interface that serves /status. Teaching the bind table longest-prefix subpath matching is the cleaner end state but changes how every route resolves to an interface, which is a refactor for a minor release, not a security patch. When that refactor lands, this path should become a regular internal-only bind and the middleware deleted. This is documented at the diagnosticsPath constant.

How the interface is identified: Go's net/http server stores the connection's local address in the request context under http.LocalAddrContextKey. Interfaces always listen on distinct ports (binding the same port twice fails at startup), so comparing the local address port with the configured internal address port identifies the arrival interface. Host is deliberately not compared, since the configured bind host (:8081) need not equal the connection's concrete local IP. If public and internal are configured to the same address there is one server and the ports always match, honoring that explicit setup. A missing or unparseable local address refuses the request (fail closed), though net/http always sets it.

Why 403 with a message instead of 404: hiding the endpoint gains nothing, its existence is public knowledge given the open source, and any behavior change equally reveals the patch level. The explicit "only available on the internal interface" message tells an operator whose external monitor probes diagnostics exactly why it broke after a patch upgrade. /status itself deliberately stays public as the liveness signal for load balancers and uptime monitors.

Testing: new TestEngine_DiagnosticsInternalOnly starts the engine on both interfaces with routes registered as the status engine does, and asserts /status returns 200 on the public interface, /status/diagnostics returns 200 with its body on the internal interface, and /status/diagnostics returns 403 without the diagnostics body on the public interface. Before the fix the public request returned 200 with the full body.

The monitoring documentation now states the endpoint is internal-only and why.

The diagnostics endpoint discloses the exact software version and git
commit (which answers "is this node patched" for version-scoped
advisories), the gRPC peer list with remote addresses and node DIDs,
and data store counts. The deployment documentation lists it as an
internal endpoint, but it was served on the public interface as well
because it shares its first path segment with /status, and interface
binding resolves on the first path segment only.

Requests reaching the path through any interface other than the
internal one are now refused with 403. The interface is identified by
comparing the port of the connection's local address (stored by
net/http under http.LocalAddrContextKey) against the configured
internal address, since interfaces always listen on distinct ports.
/status itself deliberately stays public: external load balancers and
uptime monitors probe it as a liveness signal, and breaking those in
a security patch is a bad trade.

A 403 with an explicit message is returned rather than a 404. Hiding
the endpoint gains nothing (its existence is public knowledge given
the open source), and the message tells an operator whose monitor
probes diagnostics exactly why it broke.

Assisted-by: AI
@qltysh

qltysh Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

1 new issue

Tool Category Rule Count
qlty Structure Function with many returns (count = 6): Configure 1

@qltysh

qltysh Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (1)

RatingFile% DiffUncovered Line #s
Coverage rating: B Coverage rating: B
http/engine.go80.0%116-117, 143-144
Total80.0%
🤖 Increase coverage with AI coding...
In the `fix/diagnostics-internal-only` branch, add test coverage for this new code:

- `http/engine.go` -- Lines 116-117 and 143-144

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

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