Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Protect and observe HAProxy: ACLs, stick-tables, headers, logs and Prometheus

By updated

Improve this page

HAProxy logo

Protecting HAProxy means describing in ACLs what must be rejected, counting each client's requests in a stick-table to limit abuse, and adding security headers; observing it means producing structured logs and exposing its metrics to Prometheus on an interface that is not public. This lesson follows the publication over HTTPS with HAProxy: it takes the protection and observation gestures of the training and writes them in HAProxy's syntax. The examples apply to HAProxy 3.4.7, installed from haproxy.debian.net on Debian 13.

What you will learn

The lesson goes from the rule that rejects to the metric that measures.

  • Write an ACL and apply it to a rule.
  • Require authentication on a route and reject methods.
  • Limit the rate per client with a stick-table.
  • Add security headers to responses.
  • Expose the Prometheus exporter on a restricted interface.
  • Place the runtime API and its risks.

Prerequisites

This lesson assumes you already publish a service with HAProxy.

How do you reject a request with an ACL?

An ACL names a condition, and an http-request rule uses it to act. An ACL tests an element of the request: a path, a method, a source address, a header. The associated rule rejects, redirects or modifies when the condition is true.

frontend https_in
acl chemin_admin path_beg /admin
acl reseau_interne src 10.0.0.0/8
http-request deny deny_status 403 if chemin_admin !reseau_interne

This rule answers 403 to any access to /admin that does not come from the internal network. ACLs read like sentences: name them for what they test, and keep in mind that a source address is reliable only if HAProxy is the first relay; behind another proxy, you will see the relay's address.

How do you require a password and reject methods?

A sensitive route authenticates with a userlist and http-request auth, and a useless method is rejected by an ACL on method. The userlist declares the accounts with a hashed password, never in clear text. The auth rule answers 401 as long as the http_auth condition is not satisfied.

userlist admins
user ops password $5$Xk3pQ9aB$dCdsGkSjDndrErPudW5LAbbgWQkf5eMHpYsiK0QLek9
frontend https_in
acl chemin_admin path_beg /admin
http-request auth realm Administration if chemin_admin !{ http_auth(admins) }
acl methode_ecriture method PUT DELETE
http-request deny deny_status 405 if methode_ecriture

The example's hash comes from openssl passwd -5 and matches the password Exemple-Ops-2026: generate your own. A rejection is only proven by showing what still goes through: curl -i without credentials on /admin must return 401, the same request with --user must reach the backend, a DELETE must return 405 and a GET must still succeed. A configuration that rejected everything would satisfy the first three checks by simply having removed the service.

How do you limit the rate with a stick-table?

A stick-table keeps in memory a counter per key, for example per source address, and a rule rejects the keys that exceed a threshold. Three elements are needed: declare the table and what it counts, track each request with track-sc, then reject when the counter exceeds the limit.

frontend https_in
stick-table type ip size 100k expire 30s store http_req_rate(10s)
http-request track-sc0 src
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 50 }

The table counts here the number of requests per address over a ten-second window, and the rule answers 429 above fifty. The threshold and the window are to be chosen according to the application: too low, they block legitimate users; too high, they protect nothing. As with any limitation by address, what you count must match the real client.

http_req_rate(10s) is a sliding average, not a counter that restarts from zero at a fixed interval. The 3.4 manual writes that it reports "the average HTTP request rate over that period". After a burst, the rate goes down progressively, and the client is served again only once it has fallen back under the threshold. That decay is slow: over a ten-second window and a threshold of 20, a burst of 25 requests left one control request out of two still rejected thirteen seconds after the last one. A good limit therefore releases by itself, without banning, but later than you think. Finally, the key must not be a value the client sends: a counter keyed on a header is reset by the very client it was meant to limit, hence the choice of src.

Which security headers should you add?

Add response headers that reduce the attack surface on the browser side, with http-response set-header. They tell the browser not to guess the type of a content, not to let the page be displayed in a frame, not to pass on the origin address and to use only HTTPS.

http-response set-header X-Content-Type-Options nosniff
http-response set-header X-Frame-Options DENY
http-response set-header Referrer-Policy no-referrer
http-response set-header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'"
http-response set-header Strict-Transport-Security "max-age=31536000"

Check with curl -I the headers actually received, not the ones you think you configured: a rule placed in the wrong block, or an error response produced by HAProxy itself, may not carry them. The reliable check is on a response coming from the backend, not on an error page.

How do you produce usable logs?

HAProxy can format its logs as JSON, provided the frontend has a log destination. A structured format can be filtered and aggregated without text parsing. Without a log directive on the frontend or inherited from the global section, haproxy -c warns that the format is ignored: it is the first diagnostic reflex if the lines do not appear.

defaults
mode http
log global
log-format "%{+json}o %(client_ip)ci %(status)ST %(bytes)B %(duration)Ta %(backend)b %(server)s"

Each field carries its name, which lets a tool such as Loki or Elasticsearch index them without an extraction rule.

How do you expose metrics to Prometheus?

HAProxy serves its own metrics in Prometheus format: the exporter is compiled in by default in the 3.4.7 package from haproxy.debian.net, so there is no build to do and no separate exporter to install. It is enabled by an internal service, use-service prometheus-exporter, on a dedicated frontend that must be restricted. Metrics reveal the topology of the backends and their state: they do not need to be reachable from the Internet.

frontend metriques
bind 127.0.0.1:8404
http-request use-service prometheus-exporter if { path /metrics }
no log

Bind this frontend to the address that Prometheus actually reaches, never to all interfaces. Do not enable stats enable on it: that would publish the statistics page without authentication, and this page no longer shows the version since 3.4 (stats show-version is disabled by default): read the version with haproxy -v.

First check the metrics endpoint with curl http://127.0.0.1:8404/metrics. The output must contain haproxy_frontend_status and haproxy_server_status, the latter carrying the health of each backend server. On the Prometheus side, a successful scrape shows up as a target in the up state (targets page, or the /api/v1/targets API).

What is the runtime API for?

The runtime API is a command socket that lets you read HAProxy's state and change it live, without a reload. You query the state of servers, disable a server for maintenance, look at the content of a stick-table. It is never published on the network: a Unix socket with restricted permissions, or a local listener, is enough, because whoever reaches it can change the proxy's behavior. The Debian package sets one up, /run/haproxy/admin.sock, in mode 660 and at admin level.

The socket's level decides what can be undone. An operator-level socket already runs set table and clear table: it can therefore erase a block placed by a stick-table, for example with echo "clear table https_in key 192.0.2.10" | socat stdio /run/haproxy/admin.sock. Reserve these levels for the accounts that must lift a block, and treat access to the socket as an administration right.

Practice: protect, then observe with HAProxy

Two labs are played in order: the first protects the frontend, the second observes with Prometheus. Each is validated by an automated check.

The first lab starts from a service published by HAProxy whose /admin route is served to whoever asks, which accepts an unlimited number of requests per client, which forwards DELETE and PUT to the backend and whose responses carry no security header. It asks you to add the security headers, to require credentials on /admin, to reject with 429 more than 20 requests per 10-second window and per source address, and to reject DELETE and PUT. Validation plays both ways: each rejection is checked with the request that must still go through, and the service must become available again once the window has elapsed.

The second lab starts from a service already published by HAProxy, with a Prometheus instance ready to monitor it. The metrics endpoint returns no metrics, and the two monitoring interfaces, this metrics endpoint and Prometheus's interface, are reachable from other machines. It asks you to configure the Prometheus exporter, to limit both interfaces to the host and to get Prometheus to report a target in the up state along with the backend's health metrics, without interrupting the public service.

Key points

Protecting HAProxy means rejecting by ACL and limiting by stick-table; observing it means structured logs and metrics on a closed interface.

  • An ACL names a condition, http-request deny acts.
  • userlist and http-request auth require credentials.
  • A stick-table counts per key, track-sc tracks, a threshold rejects; the rate is a sliding average that goes down.
  • http-response set-header adds the security headers.
  • The JSON log requires a log destination.
  • The metrics and the runtime API stay on the host.

Knowledge check

Knowledge check

Check what you have learned with this interactive quiz

8 questions
8 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn