What an attacker sees before they touch your site

Your /metrics endpoint is a live readout of your internals, and it's public

Almost every modern service exposes a /metrics endpoint so a monitoring system can scrape it. It's not the Prometheus dashboard, that's a UI, a separate thing. This is the raw feed underneath: a plain-text page, machine-readable, that the app emits about itself for its own monitoring to read. It's meant to be scraped by your Prometheus server on the internal network and seen by nobody else.

When that endpoint answers the public internet, it becomes a live, continuously-updating readout of your internals for anyone who appends /metrics to your URL. Nothing here is a "hack", you're just reading a page the app publishes. But the page publishes a lot, and reconnaissance is the whole reason an attacker would want it.

What the raw metrics actually leak

A /metrics response is hundreds of lines of counters and gauges, and the labels attached to them are where the intel is. Internal target hostnames and ports (the things this service talks to). Build and version info (so an attacker knows exactly which version to find a CVE for). Container and image names. The request paths your app serves, including internal and admin routes that never appear in your public navigation. And, in the worst cases, credentials in scrape-target labels, when someone configures a scrape job with a username and password in the URL, that URL shows up as a label.

request
GET /metrics HTTP/1.1 Host: yoursite.com
response
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{path="/internal/admin/users"} 4821
build_info{version="2.3.1",commit="9f2c1e7"} 1
up{instance="db-prod.internal:5432",job="postgres"} 1
Internal admin paths, the exact version, and an internal database hostname:port, all read straight off the public feed.

The scanner identifies a genuine Prometheus exposition endpoint precisely, not just any text page. It requires the response to be text/plain with the exposition format's # HELP/# TYPE lines and a Go-runtime metric name (go_*, process_cpu_seconds_total, promhttp_*), the fingerprints that every real Prometheus exporter emits. That gate means a hit is the actual metrics feed, not a coincidental page that happens to contain the word "metrics." It also checks the Spring equivalent at /actuator/prometheus.

Why "just metrics" is a real finding

It's rated high, and the reason is that this isn't one fact, it's a structured map that updates in real time. An attacker reads your version (to pick an exploit), your internal hostnames and ports (to know what's behind the front door), your route list including the unlinked admin paths (to know what to attack), and your traffic shape (to time things or spot what's busy). Each is recon, and recon is what turns a blind probe into a targeted one. The credential-in-a-label case is rarer but catastrophic when it happens: a scrape URL with user:password@ in it hands over a working login with no further work.

Gate the endpoint

/metrics should be reachable by your monitoring system and nothing else. The fix is to keep it off the public internet, the same shape as every internal endpoint.

# /metrics answers anyone on the internet
location /metrics { proxy_pass http://app; }   # open
Bind metrics to the internal network, or require auth, so only your Prometheus server can read it.

Restrict /metrics to your monitoring subnet at the reverse proxy, or, better, have the app expose metrics on a separate port bound to the internal interface so it's never web-reachable at all. Many frameworks let you put the metrics endpoint behind authentication or on a management port; use it. And keep credentials out of scrape-target URLs, use a secrets mechanism, so even an accidental exposure doesn't leak a working login.

Reading it from outside

Whether your metrics feed answers the public internet is something a stranger settles with one GET, which is exactly what SurfaceCheckr does, from outside, no credentials. It fetches /metrics (and the Spring /actuator/prometheus variant) and confirms a hit only on the real exposition format, the # HELP/# TYPE lines plus a Go-runtime metric name, so it doesn't fire on an unrelated page. It reads the feed the endpoint volunteers and stops there. This is a passive read of a page your app publishes about itself, which is exactly why it's worth checking: it's the kind of internal readout you forget is reachable until you look at it the way an outsider would. The related surfaces are the Spring Actuator endpoints that dump config and heap and the internal hostnames that leak into your client code.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.