
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.
- Publishing over HTTPS and reloading without interruption: Publish HTTPS with HAProxy 3.4: crt-store, SNI and built-in ACME certificates.
- The basic concepts of HAProxy: HAProxy.
- The path of a request and the trust placed in headers: Publish a web service.
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_interneThis 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_ecritureThe 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 nosniffhttp-response set-header X-Frame-Options DENYhttp-response set-header Referrer-Policy no-referrerhttp-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 logBind 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 denyacts. userlistandhttp-request authrequire credentials.- A stick-table counts per key,
track-sctracks, a threshold rejects; the rate is a sliding average that goes down. http-response set-headeradds 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
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
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Limit abuse with HAProxy stick-tables
A new full quiz with random questions
Work again only on the questions you missed