An HAProxy ACL lets you define custom rules for blocking malicious requests, choosing backends, redirecting to HTTPS and using cached objects.
When IT pros add load balancers into their infrastructure, they’re looking for the ability to scale out their websites and services, get better availability, and gain more restful nights, knowing that their critical services are no longer single points of failure. Before long, however, they realize that with a full-featured load balancer like HAProxy Enterprise, they can add in extra intelligence to inspect incoming traffic and make decisions on the fly. For example, you can restrict who can access various endpoints, redirect non-https traffic to HTTPS, detect and block malicious bots and scanners, and define conditions for adding HTTP headers, changing the URL, or redirecting the user.
Access Control Lists, or ACLs, in HAProxy allow you to test various conditions and perform a given action based on those tests. These conditions cover just about any aspect of a request or response such as searching for strings or patterns within them, checking the IPs they are from, recent request rates (via stick tables), TLS status, etc. The action you take can include making routing decisions, redirecting requests, returning static responses and so much more. While using logic operators (AND
, OR
, NOT
) in other proxy solutions might be cumbersome, HAProxy’s ACLs embrace them to form more complex conditions.
See The Webinar: Introduction to HAProxy ACLs: Building Rules for Dynamically Routing Requests, Redirecting Users and Blocking Malicious Traffic
Formatting an ACL
There are two ways of specifying an ACL – a named ACL and an anonymous or in-line ACL.
The first form is a named ACL:
acl is_static path -i -m beg /static/ |
We begin with the acl keyword, followed by a name, followed by the condition. Here we have an ACL named is_static
. This ACL name can then be used with if
and unless
statements such as use_backend be_static if is_static
. This form is recommended when you are going to use a given condition for multiple actions.
acl is_static path -i -m beg /static/ | |
use_backend be_static if is_static |
The condition, path -i -m beg /static/
, checks to see if the URL starts with /static/. You’ll see how that works along with other types of conditions later in this article.
The second form is an anonymous or in-line ACL:
use_backend be_static if { path -i -m beg /static/ } |
This does the same thing that the above two lines would do, just in one line. For in-line ACLs the condition is contained inside curly braces.
In both cases, you can chain multiple conditions together. ACLs listed one after another without anything in between will be considered to be joined with an and. The condition overall is only true if both ACLs are true.
http-request deny if { path -i -m beg /api/ } { src 10.0.0.0/16 } |
This will prevent any client in the 10.0.0.0/16 subnet from accessing anything starting with /api/, while still being able to access other paths.
Adding an exclamation mark inverts a condition:
http-request deny if { path -i -m beg /api/ } !{ src 10.0.0.0/16 } |
Now only clients in the 10.0.0.0/16 subnet are allowed to access paths starting with /api/ while all others will be forbidden.
The IP addresses could also be imported from a file:
http-request deny if { path -i -m beg /api/ } { src -f /etc/hapee-1.8/blacklist.acl } |
Within blacklist.acl you would then list individual or a range of IP addresses using CIDR notation to block, as follows:
192.168.122.3 | |
192.168.122.0/24 |
You can also define an ACL where either condition can be true
by using ||
:
http-request deny if { path -i -m beg /evil/ } || { path -i -m end /evil } |
With this, each request whose path starts with /evil/ (e.g. /evil/foo) or ends with /evil (e.g. /foo/evil) will be denied.
You can also do the same to combine named ACLs:
acl starts_evil path -i -m beg /evil/ | |
acl ends_evil path -i -m end /evil | |
http-request deny if starts_evil || ends_evil |
With named ACLs, specifying the same ACL name multiple times will cause a logical OR of the conditions, so the last block can also be expressed as:
acl evil path_beg /evil/ | |
acl evil path_end /evil | |
http-request deny if evil |
This allows you to combine ANDs and ORs (as well as named and in-line ACLs) to build more complicated conditions, for example:
http-request deny if evil !{ src 10.0.0.0/16 } |
This will block the request if the path starts or ends with /evil, but only for clients that are not in the 10.0.0.0/16 subnet.
Innovations such as Elastic Binary Trees or EB trees have shaped ACLs into the high performing feature they are today. For example, string and IP address matches rely on EB trees that allow ACLs to process millions of entries while maintaining the best in class performance and efficiency that HAProxy is known for.
From what we’ve seen so far, each ACL condition is broken into two parts—the source of the information (or a fetch) such as path and src and the string it is matching against. In the middle of these two parts, one can specify flags (such as -i
for a case-insensitive match) and a matching method (beg
to match on the beginning of a string, for example). All of these components of an ACL will be expanded on in the following sections.
Fetches
Now that you understand the basic way to format an ACL you might want to learn what sources of information you can use to make decisions on. A source of information in HAProxy is known as a fetch. These allow ACLs to get a piece of information to work with.
You can see the full list of fetches available in the documentation. The documentation is quite extensive and that is one of the benefits of having HAProxy Enterprise Support. It saves you time from needing to read through hundreds of pages of documentation.
Here are some of the more commonly used fetches:
| Returns the client IP address that made the request |
| Returns the path the client requested |
| Returns the value of a given URL parameter |
| Returns the value of a given HTTP request header (e.g. User-Agent or Host) |
| A boolean that returns true if the connection was made over SSL and HAProxy is locally deciphering it |
Converters
Once you have a piece of information via a fetch, you might want to transform it. Converters are separated by commas from fetches, or other converters if you have more than one, and can be chained together multiple times.
Some converters (such as lower
and upper
) are specified by themselves while others have arguments passed to them. If an argument is required it is specified in parentheses. For example, to get the value of the path with /static removed from the start of it, you can use the regsub
converter with a regex and replacement as arguments:
path,regsub(^/static,/) |
As with fetches, there are a wide variety of converters, but below are some of the more popular ones here:
| Changes the case of a sample to lowercase |
| Changes the case of a sample to uppercase |
| Base64 encodes the specified string (good for matching binary samples) |
| Allows you to extract a field similar to awk. For example if you have “a|b|c” as a sample and run field(|,3) on it you will be left with “c” |
| Extracts some bytes from an input binary sample given an offset and length as arguments |
| Looks up the sample in the specified map file and outputs the resulting value |
Flags
You can put multiple flags in a single ACL, for example:
path -i -m beg -f /etc/hapee/paths_secret.acl |
This will perform a case insensitive match based on the beginning of the path and matching against patterns stored in the specified file. There aren’t as many flags as there are fetch/converter types, but there are a nice variety.
Here are some of the commonly used ones:
| Perform a case-insensitive match (so a sample of FoO will match a pattern of Foo) |
| Instead of matching on a string, match from an ACL file. This ACL file can have lists of IP’s, strings, regexes, etc. As long as the list doesn’t contain regexes, then the file will be loaded into the b-tree format and can handle lookups of millions of items almost instantly |
| Specify the match type. This is described in detail in the next section. |
You’ll find a handful of others if you scroll down from the ACL Basics section of the documentation.
Matching Methods
Now you have a sample from converters and fetches, such as the requested URL path via path
, and something to match against via the hardcoded path /evil. To compare the former to the latter you can use one of several matching methods. As before, there are a lot of matching methods and you can see the full list by scrolling down (further than the flags) in the ACL Basics section of the documentation. Here are some commonly used matching methods:
| Perform an exact string match |
| Check the beginning of the string with the pattern, so a sample of “foobar” will match a pattern of “foo” but not “bar”. |
| Check the end of a string with the pattern, so a sample of foobar will match a pattern of bar but not foo. |
| A substring match, so a sample of foobar will match patterns foo, bar, oba. |
| The pattern is compared as a regular expression against the sample. Warning: This is CPU hungry compared to the other matching methods and should be avoided unless there is no other choice. |
| This is a match that doesn’t take a pattern at all. The match is |
| Return the length of the sample (so a sample of foo with -m len 3 will match) |
Up until this point, you may have noticed the use of path -m beg /evil/
for comparing our expected path /evil/ with the beginning of the sample we’re checking using the matching method beg
. There are a number of places where you can use a shorthand that combines a sample fetch and a matching method in one argument. In this example path_beg /foo/
and path -m beg /foo/
are exactly the same, but the former is easier to type and read. Not all fetches have variants with built-in matching methods (in fact, most don’t), and there’s a restriction that if you chain a fetch with a converter you have to specify it using a flag (unless the last converter on the chain has a match variant, which most don’t).
If there isn’t a fetch variant of the desired matching method or if you are using converters you can use the -m
flag noted in the previous section to specify the matching method.
Things to Do With Acls
Now that you know how to define ACLs, let’s get a quick idea for the common actions in HAProxy that can be controlled by ACLs. This isn’t meant to give you a complete list of all the conditions or ways that these rules can be used, but rather provide fuel to your imagination for when you encounter something with which ACLs can help.
Redirecting a request with http-request redirect
There are a number of variants of this rule, all of which return a 301/302 response to the client telling them to request under another path. All of these allow for log-format rules, specified using the %[]
syntax, to be used in the strings to allow for dynamic redirects.
The command http-request redirect location
sets the entire URI. For example to redirect non-www domains to their www variant you can use:
http-request redirect location http://www.%[hdr(host)]%[capture.req.uri] unless { hdr_beg(host) -i www } |
In this case, our ACL, hdr_beg(host) -i www
, ensures that the client is redirected unless their Host HTTP header already begins with www.
The command http-request redirect scheme
changes the scheme of the request while leaving the rest alone. This allows for trivial http-to-https redirect lines:
redirect scheme https if !{ ssl_fc } |
Here, our ACL !{ ssl_fc }
checks whether the request did not come in over HTTPS.
The command http-request redirect prefix
allows you to specify a prefix to redirect the request to. For example, the following line causes all requests that don’t have a URL path beginning with /foo to be redirected to /foo/{original URI here}.:
http-request redirect prefix /foo if !{ path_beg /foo/ } |
For each of these a code argument can be added to specify a response code. If not specified it defaults to 302. Supported response codes are 301, 302, 303, 307, and 308. For example:
redirect scheme code 301 https if !{ ssl_fc } |
This will redirect HTTP requests to HTTPS and tell clients that they shouldn’t keep trying HTTP. Or for a more secure version of this, you could inject the Strict-Transport-Security header via http-response set-header
.
Selecting a Backend With use_backend
In HTTP mode
The use_backend
line allows you to specify conditions for using another backend. For example, to send traffic for the HAProxy stats webpage to a stats backend, you can combine use_backend
with an ACL that checks whether the URL path begins with /stats:
use_backend be_stats if { path_beg /stats } |
To get even more interesting, the backend name can be dynamic with log-format rules. In the following example, we put the path through a map and use that to generate the backend name:
use_backend be_%[path,map_beg(/etc/hapee-1.8/paths.map)] |
If the file paths.map contains /api api as a key-value pair, then traffic will be sent to be_api, combining the static be_ with the string API. If none of the map entries match and you’ve specified the optional second parameter, the default argument, to the map function, then that default will be used.
use_backend be_%[path,map_beg(/etc/hapee-1.8/paths.map, mydefault)] |
In this case, if there isn’t a match in the map file, then the backend be_mydefault will be used. Otherwise, traffic will automatically fall-through this rule in search of another use_backend
rule that matches or the default_backend line otherwise.
In TCP mode
We can also make routing decisions for TCP mode traffic, for example directing traffic to a special backend if the traffic is SSL:
tcp-request inspect-delay 10s | |
use_backend be_ssl if { req.ssl_hello_type gt 0 } |
Gist URL *
URL for the Gist
Gist file
Gists file location
Note that for tcp-level routing decisions, when requiring data from the client such as needing to inspect the request, the inspect-delay
statement is required to avoid HAProxy passing the phase by without any data from the client yet. It won’t wait the full 10 seconds unless the client stays silent for 10 seconds. It will move ahead as soon as it can decide whether the buffer has an SSL hello message of some type or not.
Setting an HTTP Header With Http-Request Set-Header
There are a variety of options for adding an HTTP header to the request (transparently to the client). Combining this with an ACL lets us only set the header if a given condition is true.
| Adds a new header. If a header of the same name was sent by the client this will ignore it, adding a second header of the same name. |
| Will add a new header in the same way as |
| Applies a regex replacement of the named header (injecting a fake cookie into a cookie header, for example) |
| Deletes any header by the specified name from the request. Useful for removing an x-forwarded-for header before |
Changing the URL With Http-Request Set-Path
This allows HAProxy to modify the path that the client requested, but transparently to the client. Its value accepts log-format rules so you can make the requested path dynamic. For example, if you wanted to add /foo/ to all requests (as in the redirect example above) without notifying the client of this, use:
http-request set-path /foo%[path] if !{ path_beg /foo } |
There are also set-query
, which changes the query string instead of the path, and set-uri
, which sets the path and query string together, variants of this.
Updating Map Files With Http-Response Set-Map
These actions aren’t used very frequently, but open up interesting possibilities in dynamically adjusting HAProxy maps. This can be used for tasks such as having a login server tell HAProxy to send a clients’ (in this case by session cookie) requests to another backend from then on:
http-request set-var(txn.session_id) cook(sessionid) | |
use_backend be_%[var(txn.session_id),map(/etc/hapee-1.8/sessionid.map)] if { var(txn.session_id),map(/etc/hapee-1.8/sessionid.map) -m found } | |
http-response set-map(/etc/hapee-1.8/sessionid.map) %[var(txn.session_id)] %[res.hdr(x-new-backend)] if { res.hdr(x-new-backend) -m found } | |
default_backend be_login |
Now if a backend sets the x-new-backend header in a response, HAProxy will send subsequent requests with the client’s sessionid cookie to the specified backend. Variables are used as, otherwise, the request cookies are inaccessible by HAProxy during the response phase—a solution you may want to keep in mind for other similar problems that HAProxy will warn about during startup.
There is also the related del-map
to delete a map entry based on an ACL condition.
As with most actions, http-response set-map
has a related action called http-request set-map
. This is useful as a pseudo API to allow backends to add and remove map entries.
Caching With Http-Request Cache-Use
New to HAProxy 1.8 is small object caching, allowing the caching of resources based on ACLs. This, along with http-response cache-store
, allows you to store select requests in HAProxy’s cache system. For example, given that we’ve defined a cache named icons, the following will store responses from paths beginning with /icons/ and reuse them in future requests:
http-request set-var(txn.path) path | |
acl is_icons_path var(txn.path) -m beg /icons/ | |
http-request cache-use icons if is_icons_path | |
http-response cache-store icons if is_icons_path |
Using ACLs to Block Requests
Now that you’ve familiarized yourself with ACLs, it’s time to do some request blocking!
The command http-request deny
returns a 403 to the client and immediately stops processing the request. This is frequently used for DDoS mitigation and bot management, as HAProxy can deny a very large volume of requests without bothering the web server.
Other responses similar to this include http-request tarpit
(keep the request hanging until timeout tarpit expires, then return a 500 – good for slowing down bots by overloading their connection tables, if there aren’t too many of them), http-request silent-drop
(have HAProxy stop processing the request but tell the kernel to not notify the client of this – leaves the connection from a client perspective open, but closed from the HAProxy perspective; be aware of stateful firewalls).
With both deny and tarpit you can add the deny_status
flag to set a custom response code instead of the default 403/500 that they use out of the box. For example using http-request deny deny_status 429
will cause HAProxy to respond to the client with the error 429: Too Many Requests.
In the following subsections we will provide a number of static conditions for which blocking traffic can be useful.
HTTP protocol version
A number of attacks use HTTP 1.0 as the protocol version, so if that is the case it’s easy to block these attacks using the built-in ACL HTTP_1.0
:
http-request deny if HTTP_1.0 |
Contents of the user-agent string
We can also inspect the User-Agent header and deny if it matches a specified string.
http-request deny if { req.hdr(user-agent) -m sub evil } |
This line will deny the request if the -m sub
part of the user-agent request header contains the string evil anywhere in it. Remove the -m sub
, leaving you with req.hdr(user-agent) evil
as the condition, and it will be an exact match instead of a substring.
Length of the user-agent string
Some attackers will attempt to bypass normal user agent strings by using a random md5sum, which can be identified by length and immediately blocked:
http-request deny if { req.hdr(user-agent) -m len 32 } |
Attackers can vary more with their attacks, so you can rely on the fact that legitimate user agents are longer while also being set to a minimum length:
http-request deny if { req.hdr(user-agent) -m len le 32 } |
This will then block any requests which have a user-agent header shorter than 32 characters.
Path
If an attacker is abusing a specific URL that legitimate clients don’t, one can block based on path:
http-request deny if { path /api/wastetime } |
Or you can prevent an attacker from accessing hidden files or folders:
http-request deny if { path -m sub /. } |
Updating ACL Lists
Using lb-update
ACL files are updated when HAProxy is reloaded to read the new configuration, but it is also possible to update its contents during runtime.
HAProxy Enterprise (HAPEE) ships with a native module called lb-update that can be used with the following configuration:
dynamic-update | |
update id /etc/hapee-1.8/whitelist.acl url http://192.168.122.1/whitelist.acl delay 60s |
HAProxy Enterprise will now update the ACL contents every 60 seconds by requesting the specified URL. Support also exists for retrieving the URL via HTTPS and using client certificate authentication.
Using the runtime API
To update the configuration during runtime, simply use the Runtime API to issue commands such as the following:
echo "add acl /etc/hapee-1.8/whitelist.acl 1.2.3.4" | socat stdio /var/run/hapee-lb.sock |
More information on the HAProxy Runtime API can be found in one of our previous blog posts titled Dynamic Configuration with the HAProxy Runtime API.
Conclusion
That’s all folks! We have provided you with some examples to show the power within the HAProxy ACL system. The above list isn’t exhaustive or anywhere near complete, but it should give you the building blocks needed to solve a vast array of problems you may encounter quickly and easily. Use your imagination and experiment with ACLs.
Got a specific use case for ACLs you’d like to share? Post it below! Or contact support for help with advanced usage. Sign up for a trial of HAProxy Enterprise – Trial Version or contact us to get advanced features like the ability to automatically update ACL rules with lb-update.
Stay tuned by signing up for blog updates!