HAProxy ACLs let you route traffic to different backend servers based on incredibly granular criteria, but the most surprising thing is how often they’re used to prevent traffic from reaching a backend, rather than direct it.
Let’s see this in action. Imagine you have a frontend listening on port 80, and you want to send traffic to different backend pools based on the URL path.
frontend http_in
bind *:80
acl is_api_path path_beg /api/v1
acl is_static_path path_beg /static
acl is_admin_path path_beg /admin
use_backend api_servers if is_api_path
use_backend static_servers if is_static_path
use_backend admin_servers if is_admin_path
default_backend web_servers
backend api_servers
server api1 192.168.1.10:8080 check
server api2 192.168.1.11:8080 check
backend static_servers
server static1 192.168.1.20:80 check
server static2 192.168.1.21:80 check
backend admin_servers
server admin1 192.168.1.30:8000 check
backend web_servers
server web1 192.168.1.40:80 check
server web2 192.168.1.41:80 check
Here’s what’s happening:
frontend http_in: This is where traffic arrives. It listens on all interfaces (*) on port 80.acl is_api_path path_beg /api/v1: This defines an Access Control List (ACL) namedis_api_path. It’s true if the request’s URI begins with/api/v1.acl is_static_path path_beg /static: Similar to the above, this ACL is true if the URI starts with/static.acl is_admin_path path_beg /admin: This ACL matches URIs starting with/admin.use_backend api_servers if is_api_path: This is the routing rule. If theis_api_pathACL is true, HAProxy will send the request to theapi_serversbackend. Theifkeyword is crucial here; it means "use this backend if this condition is met."use_backend static_servers if is_static_path: If the previous rule didn’t match, and theis_static_pathACL is true, traffic goes tostatic_servers.use_backend admin_servers if is_admin_path: If neither of the above matched, and theis_admin_pathACL is true, traffic goes toadmin_servers.default_backend web_servers: This is the fallback. If none of the precedinguse_backendrules matched, the request will be sent toweb_servers. This is how you ensure all traffic is handled.
The use_backend directives are evaluated in order. The first one that matches wins. This is why you can build complex routing logic. You could also use path_end, path_dir, path_reg (for regular expressions), hdr (for HTTP headers), src (for source IP), cook (for cookies), and many more.
The power of ACLs lies in their composability. You can combine multiple ACLs using logical operators like AND (implicit when listed sequentially) or OR (explicitly using acl name or acl_other). For example, to send traffic from a specific IP range to the admin panel only if it’s also hitting the /admin path, you’d do:
acl admin_ips src 192.168.0.0/24
acl is_admin_path path_beg /admin
acl allowed_admin_access admin_ips AND is_admin_path
use_backend admin_servers if allowed_admin_access
This admin_ips AND is_admin_path creates a new ACL allowed_admin_access that is only true when both conditions are met.
Many people don’t realize that you can use ACLs to deny traffic entirely by sending it to a backend that has no servers. A common pattern is to block specific malicious user agents or known bad IPs.
acl bad_user_agent hdr(User-Agent) -i "nmap" "sqlmap"
acl blocked_ip src 1.2.3.4
block if bad_user_agent or blocked_ip
In this snippet, block is a special action in HAProxy that simply rejects the request with an appropriate HTTP error (usually 403 Forbidden) without sending it to any backend. This is incredibly efficient for filtering at the edge.
When you define multiple use_backend rules, HAProxy iterates through them in order until a condition is met. If no use_backend rule matches, it falls back to the default_backend. This sequential evaluation is key to understanding how complex routing scenarios are resolved.