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) named is_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 the is_api_path ACL is true, HAProxy will send the request to the api_servers backend. The if keyword 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 the is_static_path ACL is true, traffic goes to static_servers.
  • use_backend admin_servers if is_admin_path: If neither of the above matched, and the is_admin_path ACL is true, traffic goes to admin_servers.
  • default_backend web_servers: This is the fallback. If none of the preceding use_backend rules matched, the request will be sent to web_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.

Want structured learning?

Take the full Haproxy course →