The HAProxy stats page is the operator’s primary live view of the load balancer — frontends, backends, server states, request rates, queue depths, all on one page that refreshes itself. It is also the most casually-exposed admin surface we see on inherited systems. This article covers the safe production setup: dedicated frontend, scoped bind, real auth, and a CSV export endpoint for scrapers.
How to verify
If you inherited a config and don’t know whether or how the stats page is exposed, find it:
grep -nE 'stats (uri|enable|admin|auth)' /etc/haproxy/haproxy.cfg
ss -ltnp | grep haproxy
curl -I http://127.0.0.1:8404/
curl -u admin:placeholder -s http://127.0.0.1:8404/?stats;csv | head -3
The first grep enumerates every stats directive. The ss line shows what HAProxy is listening on. If :8404 is bound on 0.0.0.0 with no auth, you have an exposure to fix before you finish this article.
What’s happening
The stats page is served by HAProxy itself — there is no separate process. It is enabled by:
- A
bindline on a frontend or listener (where browsers connect), stats enable(turns the page on),stats uri /(the URL path; defaults to/haproxy?stats),- Optional
stats auth user:pass(HTTP basic), - Optional
stats admin if {acl}(enables the admin actions like disable/enable server), - Optional
stats refresh 10s(auto-refresh interval).
The page is also served as CSV when you append ;csv to the URL — that endpoint is what Prometheus scrapers, custom dashboards, and CLI tools (echo "show stat" | socat ...) consume.
The page is unauthenticated by default. stats auth adds HTTP basic. Anything more than basic (mTLS, OAuth proxy) lives in a separate frontend that proxies to the stats listener.
The procedure
-
Dedicated frontend, narrow bind. Put the stats page on its own port and bind it to the internal management address, not the public IP:
frontend fe_stats bind 10.0.10.5:8404 mode http option httplog stats enable stats uri / stats refresh 10s stats show-node stats show-legends stats auth admin:replace-this-with-a-real-secretThe bind to
10.0.10.5:8404(your management VLAN) means the page is reachable only from inside that network. UFW or your security group should reinforce this — the stats listener is not part of the public attack surface. -
Add admin actions behind a stricter ACL. With
stats admin, the page exposes buttons to disable / enable / drain servers — this is power. Gate it to a tighter source range:frontend fe_stats bind 10.0.10.5:8404 acl admin_net src 10.0.10.0/24 stats enable stats uri / stats refresh 10s stats auth admin:replace-this stats admin if admin_netWithout
stats admin, the page is read-only. We default to read-only and grant admin only to specific jumpboxes. -
Hide internal hostnames.
stats show-nodedisplays the HAProxy node’s hostname. If your hostnames leak detail about the environment, disable it:stats hide-version no stats show-node -
CSV scrape endpoint. Many observability tools consume the CSV directly. The CSV path is the same URI with
;csvappended:curl -u admin:secret -s "http://10.0.10.5:8404/?stats;csv" | head -5Prometheus integration uses
haproxy_exporterwhich fetches the CSV and re-exposes it as metrics. The native Prometheus endpoint exists in HAProxy 2.0+:frontend fe_stats bind 10.0.10.5:8404 http-request use-service prometheus-exporter if { path /metrics } stats enable stats uri /The
prometheus-exporterservice ships in HAProxy 2.0+ as a built-in. Set up a separate path (/metrics) and a Prometheus scrape job points at it. -
Auth that survives a config audit. The
stats auth user:passline stores the password in plaintext inhaproxy.cfg. We pull the credential from a separate config file kept out of Git (or a HashiCorp Vault snippet) andincludeit:sudo touch /etc/haproxy/stats-auth.conf sudo chmod 600 /etc/haproxy/stats-auth.conf sudo chown root:haproxy /etc/haproxy/stats-auth.conffrontend fe_stats bind 10.0.10.5:8404 stats enable stats uri / .if defined(STATS_AUTH) stats auth $(STATS_AUTH) .endifHAProxy 2.4+ supports environment-variable substitution; older versions require an
includedirective. -
Front-end stats vs runtime API. The stats page is for humans. The runtime API (
/run/haproxy/admin.sock) is for scripts. They expose overlapping data but the runtime API is faster and more granular — see HAProxy runtime API.
Common pitfalls
- Binding the stats page to
*:8404with no auth is the most common misconfiguration we find in inherited systems. The page leaks backend names, IPs, and lets anyone trigger reconfigs ifstats adminis on. stats authis HTTP basic with cleartext password in the config file. If you shiphaproxy.cfgto a Git repo, the password is in Git. Alwaysincludethe auth line from a chmod 600 file outside Git.stats refresh 5son a popular stats page on a busy load balancer is wasted CPU. 30 seconds is plenty for a dashboard; for live debugging, the runtime API is faster.- The Prometheus
/metricsendpoint shares the same listener as the stats page. If you locked down the stats page with auth, the metrics endpoint inherits the auth — which breaks scrapers expecting an unauthenticated path. Either keep them on the same frontend withhttp-request auth realm Stats if !{ path /metrics }to skip auth on metrics, or put metrics on a separate frontend. stats adminlets a logged-in user drain a backend server with one click. We have seen on-call engineers triage an incident and accidentally drain the wrong cluster. Default to read-only; require a ticket-driven session to enable admin.
Stack Harbor wires the stats page on its own management frontend, with auth pulled from secret storage and admin actions limited to bastion source IPs. The dashboards that the team actually uses pull from the Prometheus endpoint, not the HTML page; the HTML is for incident triage. This is part of our Managed Operations baseline.