Skip to content

HAProxy stats page

Configure the HAProxy stats page safely — bind, auth, scope, CSV export, and the operational view we expose to engineers without exposing it to the internet.

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 bind line 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

  1. 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-secret

    The 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.

  2. 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_net

    Without stats admin, the page is read-only. We default to read-only and grant admin only to specific jumpboxes.

  3. Hide internal hostnames. stats show-node displays the HAProxy node’s hostname. If your hostnames leak detail about the environment, disable it:

    stats hide-version
    no stats show-node
  4. CSV scrape endpoint. Many observability tools consume the CSV directly. The CSV path is the same URI with ;csv appended:

    curl -u admin:secret -s "http://10.0.10.5:8404/?stats;csv" | head -5

    Prometheus integration uses haproxy_exporter which 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-exporter service ships in HAProxy 2.0+ as a built-in. Set up a separate path (/metrics) and a Prometheus scrape job points at it.

  5. Auth that survives a config audit. The stats auth user:pass line stores the password in plaintext in haproxy.cfg. We pull the credential from a separate config file kept out of Git (or a HashiCorp Vault snippet) and include it:

    sudo touch /etc/haproxy/stats-auth.conf
    sudo chmod 600 /etc/haproxy/stats-auth.conf
    sudo chown root:haproxy /etc/haproxy/stats-auth.conf
    frontend fe_stats
        bind 10.0.10.5:8404
        stats enable
        stats uri /
        .if defined(STATS_AUTH)
        stats auth $(STATS_AUTH)
        .endif

    HAProxy 2.4+ supports environment-variable substitution; older versions require an include directive.

  6. 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 *:8404 with no auth is the most common misconfiguration we find in inherited systems. The page leaks backend names, IPs, and lets anyone trigger reconfigs if stats admin is on.
  • stats auth is HTTP basic with cleartext password in the config file. If you ship haproxy.cfg to a Git repo, the password is in Git. Always include the auth line from a chmod 600 file outside Git.
  • stats refresh 5s on 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 /metrics endpoint 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 with http-request auth realm Stats if !{ path /metrics } to skip auth on metrics, or put metrics on a separate frontend.
  • stats admin lets 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.