Security
Traffic policing
Traffic policing allows you to limit the rate and number of requests flowing to your backend servers. Traffic policing measures can ensure that users get the desired quality of service, and they can even prevent malicious traffic such as DDoS attacks.
In practice, traffic policing involves denying requests when request rates or counts exceed specified thresholds.
Queue connections to servers Jump to heading
It’s possible to use connection queueing to achieve the desired level of fairness without resorting to rate limiting. With connection queueing, the proxy stores excess connections until the servers are freed up to handle them. The load balancer is designed to hold lots of connections without a sharp increase in memory or CPU usage.
Queueing is disabled by default. To enable it:
-
Add the
maxconnargument toserverdirectives. Use themaxconnargument to specify the maximum number of concurrent connections that will be established with the server.In the following example, up to 30 connections will be established to each server. Once all servers reach their maximum number of connections, new connections queue up in the load balancer:
haproxybackend serversserver s1 192.168.30.10:80 check maxconn 30server s2 192.168.31.10:80 check maxconn 30server s3 192.168.31.10:80 check maxconn 30haproxybackend serversserver s1 192.168.30.10:80 check maxconn 30server s2 192.168.31.10:80 check maxconn 30server s3 192.168.31.10:80 check maxconn 30With this configuration, at most 90 connections can be active at a time. New connections will be queued on the proxy until an active connection closes.
-
To define how long clients can remain in the queue, add the
timeout queuedirective:haproxybackend serverstimeout queue 10sserver s1 192.168.30.10:80 check maxconn 30server s2 192.168.31.10:80 check maxconn 30server s3 192.168.31.10:80 check maxconn 30haproxybackend serverstimeout queue 10sserver s1 192.168.30.10:80 check maxconn 30server s2 192.168.31.10:80 check maxconn 30server s3 192.168.31.10:80 check maxconn 30If a connection request still cannot be dispatched within the
timeoutperiod, the client receives a503 Service Unavailableerror. This error response is generally more desirable than allowing servers to become overwhelmed. From the client’s perspective, it’s better to receive a timely error that can be handled programmatically than to wait an extended amount of time and possibly cause errors that are more difficult to resolve.
Rate limit by IP address Jump to heading
To rate limit HTTP requests where you track clients by their IP addresses, define a stick table that has a type of ipv6, which can store IPv4 and IPv6 addresses. Use the fetch method sc_http_req_rate(<stick counter>) to fetch the current client’s rate. In the following example, we deny clients that make more than 100 requests in 10 seconds. The client receives a 429 Too Many Requests response. The time period of 10 seconds ends with the current request and is a sliding window. As requests age, they move out of the window, and the client is able to again make requests.
haproxypeers mypeerspeer local 127.0.0.1:10000table client_request_rates type ipv6 size 1m expire 10s store http_req_rate(10s)frontend websitebind :80http-request track-sc0 src table mypeers/client_request_ratesacl exceeds_limit sc_http_req_rate(0,mypeers/client_request_rates) gt 100http-request deny deny_status 429 if exceeds_limitdefault_backend servers
haproxypeers mypeerspeer local 127.0.0.1:10000table client_request_rates type ipv6 size 1m expire 10s store http_req_rate(10s)frontend websitebind :80http-request track-sc0 src table mypeers/client_request_ratesacl exceeds_limit sc_http_req_rate(0,mypeers/client_request_rates) gt 100http-request deny deny_status 429 if exceeds_limitdefault_backend servers
Depending on the period — here it’s 10 seconds — and how frequently the client makes requests, they may continue to exceed the rate limit for the current sliding window period. They might be blocked indefinitely if they don’t slow down for long enough. There’s another way to restrict clients that avoids accidentally denying them for a long time: Once the client exceeds the rate limit, flag them and deny them for a specific period of time. This works best when the deny period is longer than the sliding window of the rate limit period, giving the client time to back off, but without blocking them indefinitely.
Let’s see an example of that. In the next configuration snippet, we define two stick tables. One tracks each client’s request rate, while the other tracks whether they’ve been flagged as exceeding the limit. Notice that once a client has been flagged, we deny them. But to avoid denying them indefinitely, the http-request deny directive comes before the http-request track-sc0 and http-request track-sc1 directives, which short-circuits the processing and stops tracking the client’s requests until the record in the flagged_clients stick table expires and is removed. The converters that begin with table_, such as table_http_req_rate and table_gpt, can fetch a record from a stick table even when we skip tracking the current request, since you feed the source IP in via the src fetch.
Clients are flagged if they make more than 100 requests in 10 seconds. They’re denied then for 30 seconds, which is the value of the expire argument on the flagged_clients stick table. We return an HTTP header named Retry-After that tells the client how long in seconds until they can try again, based on the time until their stick table record expires. For that, use the table_expire converter. It returns milliseconds, so divide by 1,000.
haproxypeers mypeerspeer local 127.0.0.1:10000table client_request_rates type ipv6 size 1m expire 10s store http_req_rate(10s)table flagged_clients type ipv6 size 1m expire 30s store gpt(1)frontend websitebind :80acl exceeds_limit src,table_http_req_rate(mypeers/client_request_rates) gt 100acl is_flagged src,table_gpt(0,mypeers/flagged_clients) eq 1http-request deny deny_status 429 if is_flaggedhttp-after-response set-header Retry-After %[src,table_expire(mypeers/flagged_clients),div(1000)] if is_flaggedhttp-request track-sc0 src table mypeers/client_request_rateshttp-request track-sc1 src table mypeers/flagged_clientshttp-request sc-set-gpt(0,1) 1 if exceeds_limitdefault_backend servers
haproxypeers mypeerspeer local 127.0.0.1:10000table client_request_rates type ipv6 size 1m expire 10s store http_req_rate(10s)table flagged_clients type ipv6 size 1m expire 30s store gpt(1)frontend websitebind :80acl exceeds_limit src,table_http_req_rate(mypeers/client_request_rates) gt 100acl is_flagged src,table_gpt(0,mypeers/flagged_clients) eq 1http-request deny deny_status 429 if is_flaggedhttp-after-response set-header Retry-After %[src,table_expire(mypeers/flagged_clients),div(1000)] if is_flaggedhttp-request track-sc0 src table mypeers/client_request_rateshttp-request track-sc1 src table mypeers/flagged_clientshttp-request sc-set-gpt(0,1) 1 if exceeds_limitdefault_backend servers
Rate limit by URL path Jump to heading
You can assign distinct rate limits to individual URLs of your web application. This type of configuration can be useful when different pages require different amounts of processing time, and thus can handle a different number of concurrent users. This configuration uses a map file to associate different rate limits to different URLs in your web application.
-
On the load balancer, create a file called
rates.map. -
In the file, list the URL paths and rate thresholds. For example:
haproxy/urla 10/urlb 20/urlc 30haproxy/urla 10/urlb 20/urlc 30 -
Update the
frontendconfiguration to include thestick-tableandhttp-request trackdirectives shown below:haproxyfrontend websitebind :80stick-table type binary len 20 size 100k expire 10s store http_req_rate(10s)http-request track-sc0 base32+srchttp-request set-var(req.rate_limit) path,map_beg(/rates.map,20)http-request set-var(req.request_rate) base32+src,table_http_req_rate()acl exceeds_limit var(req.rate_limit),sub(req.request_rate) lt 0http-request deny deny_status 429 if exceeds_limitdefault_backend servershaproxyfrontend websitebind :80stick-table type binary len 20 size 100k expire 10s store http_req_rate(10s)http-request track-sc0 base32+srchttp-request set-var(req.rate_limit) path,map_beg(/rates.map,20)http-request set-var(req.request_rate) base32+src,table_http_req_rate()acl exceeds_limit var(req.rate_limit),sub(req.request_rate) lt 0http-request deny deny_status 429 if exceeds_limitdefault_backend serversIn this example:
- The stick table has a key of
binaryto match the tracked value generated by thehttp-request track-sc0 base32+srcdirective, which is a hash of the HTTP Host header, the URL path, and the client’s source IP address. This key allows the load balancer to differentiate request rates across all different web pages. - The
http-request set-var(req.rate_limit)directive retrieves the rate limit threshold from therates.mapfile. This directive finds the request rate threshold in therates.mapfile for the current URL path being requested. If the URL isn’t in the map file, a default value of 20 is used. The resulting threshold value is stored in the variablereq.rate_limit. - The
http-request set-var(req.request_rate)directive records the client’s request rate. - The ACL named
exceeds_limitis set totrueif the client’s request rate is greater than the rate limit threshold. - If the threshold is exceeded, the
http-request denydirective denies the request.
- The stick table has a key of
Rate limit by URL parameter Jump to heading
As an alternative to rate limiting by URL path, you can configure request rate limiting by URL parameter. This approach can be useful if your clients include an API token in the URL to identify themselves. This configuration is based on a sliding window rate limit configuration.
In the following example, the client is expected to include a token with their requests, as follows:
texthttp://yourwebsite.com/api/v1/does_a_thing?token=abcd1234
texthttp://yourwebsite.com/api/v1/does_a_thing?token=abcd1234
For this example, the configuration applies a limit of 1,000 requests per 24 hour period, and it also requires that the user supply a token as shown above.
-
In the frontend, add a stick table with a
typeofstringto store the HTTP request rate. The sliding window size in this example is 24 hours:haproxyfrontend websitebind :80stick-table type string size 100k expire 24h store http_req_rate(24h)acl has_token url_param(token) -m foundacl exceeds_limit url_param(token),table_http_req_rate() gt 1000http-request track-sc0 url_param(token) unless exceeds_limithttp-request deny deny_status 429 if !has_token or exceeds_limithaproxyfrontend websitebind :80stick-table type string size 100k expire 24h store http_req_rate(24h)acl has_token url_param(token) -m foundacl exceeds_limit url_param(token),table_http_req_rate() gt 1000http-request track-sc0 url_param(token) unless exceeds_limithttp-request deny deny_status 429 if !has_token or exceeds_limitIn this example:
- The ACL named
has_tokenindicates if the desired token is included in the URL. - The ACL named
exceeds_limitfinds the current request count for the last 24 hours and compares it to the request rate limit threshold,1000. - The
http-request trackdirective stores the value of the URL parameter namedtokenas the key in the table. Theunless exceeds_limitclause serves an important purpose. It prevents the counter from continuing to increment once the client has exceeded the limit. The clause also allows the entry to expire so that the client isn’t permanently blocked. - The
http-request denydirective denies the request if the token is missing or if the limit is exceeded.
- The ACL named
There is an important reason why this configuration uses the http_req_rate(24h) counter instead of the http_req_cnt counter in conjunction with an expire parameter set to 24h. The former is a sliding window over the last 24 hours. The latter begins when the user sends their first request and increments from then on until the expiration. However, unless you’re manually clearing the table every 24 hours via the Runtime API, the http_req_cnt could stay in effect for a long time while the client stays active. That’s because the expiration is reset whenever the record is touched.
Limit requests per day (sliding window) Jump to heading
You can limit the number of HTTP requests a user can make within a period of time, such as 24 hours. The period of time ends with the current request and includes requests that happened during the preceding period. This kind of limit is called a sliding window rate limit. It works the same way as rate limiting by IP address, but with a longer period.
In this example, the sliding window limit allows each client to issue no more than 1000 requests during a 24-hour period:
haproxyfrontend websitebind :80stick-table type ipv6 size 1m expire 24h store http_req_rate(24h)http-request track-sc0 srchttp-request deny deny_status 429 if { sc_http_req_rate(0) gt 1000 }default_backend servers
haproxyfrontend websitebind :80stick-table type ipv6 size 1m expire 24h store http_req_rate(24h)http-request track-sc0 srchttp-request deny deny_status 429 if { sc_http_req_rate(0) gt 1000 }default_backend servers
-
With the
stick-tabledirective, you define a table that stores each client’s HTTP request rate over the given period, where each client is tracked by IP address. To conserve space, the stick table size is set to the1mmost recently active clients, where1mmeans 1,048,576. Also, entries expire and are removed if they are inactive for 24 hours, ensuring we keep a record for the same amount of time as the sliding window request rate period. -
The
http-request trackdirective stores the client’s IP address with their request rate in the stick table. -
The
http-request denydirective denies requests for clients that exceed the limit. On thehttp-request denyline, theifexpression determines whether the client’s current request rate has exceeded the allowed number of requests, in this case 1000. If so, we deny the current request with a429 Too Many Requestsresponse. When the count of requests during the preceding 24 hours is again below 1000, we accept the request.
You can adjust any part of this example to suit your needs.
-
To change the time period, change the time specified in the
http_req_ratefetch in thestick-tabledirective. For a one-minute period, use1m:haproxystick-table type ipv6 size 1m expire 1m store http_req_rate(1m)haproxystick-table type ipv6 size 1m expire 1m store http_req_rate(1m) -
To change the number of allowable requests in the interval, change the
gttest value specified in thehttp-request denydirective. For 10,000 requests, set it like this:haproxyhttp-request deny deny_status 429 if { sc_http_req_rate(0) gt 10000 }haproxyhttp-request deny deny_status 429 if { sc_http_req_rate(0) gt 10000 }
Limit requests per day (fixed window) Jump to heading
Limitations
This technique requires the HAProxy Runtime API, which isn’t available with HAProxy ALOHA.
A fixed window request limit restricts the number of requests that a client can send during a fixed period of time, such as a calendar day. In this example, we configure a limit of 1,000 HTTP requests during a calendar day. The http_req_cnt counter stores the running total of requests each client makes, based on IP address, as a 32-bit integer.
haproxyfrontend websitebind :80stick-table type ipv6 size 100k expire 24h store http_req_cnthttp-request track-sc0 srchttp-request deny deny_status 429 if { sc_http_req_cnt(0) gt 1000 }default_backend servers
haproxyfrontend websitebind :80stick-table type ipv6 size 100k expire 24h store http_req_cnthttp-request track-sc0 srchttp-request deny deny_status 429 if { sc_http_req_cnt(0) gt 1000 }default_backend servers
-
In the frontend, the
stick-tabledirective defines the data we want to store. It has a type ofipv6to store IPv4 and IPv6 client IP addresses. -
The
http-request trackdirective stores the client’s IP address with their request count in the stick table. With each new request, the count increments. -
The
http-request denydirective denies requests for clients that exceed the limit.
This configuration causes every request after 1,000 to be denied, but we need to reset that restriction at midnight. To reset the counter, use the Runtime API’s clear table command:
-
Install the
socatutility, and use it to invoke theclear tableRuntime API command to clear all records from the stick table:nixecho "clear table website" |\sudo socat stdio unix-connect:/var/run/hapee-3.3/hapee-lb.socknixecho "clear table website" |\sudo socat stdio unix-connect:/var/run/hapee-3.3/hapee-lb.sock -
To reset the counter automatically, set up a daily
cronjob.
Other response policies Jump to heading
So far, our examples have used http-request deny to drop HTTP requests that exceed the rate limit. However, there are other policies you can implement.
Deny an HTTP request Jump to heading
You can deny a client’s HTTP request to return a 403 Forbidden error to the client. Use the directive http-request deny in your frontend section. In the example below, we deny the client’s request if they’ve made more than 20 requests within the last minute:
haproxyfrontend websitebind :80# use a stick table to track request ratesstick-table type ip size 100k expire 1m store http_req_rate(1m)http-request track-sc0 srcacl too_many_requests sc_http_req_rate(0) gt 20http-request deny if too_many_requestsdefault_backend webservers
haproxyfrontend websitebind :80# use a stick table to track request ratesstick-table type ip size 100k expire 1m store http_req_rate(1m)http-request track-sc0 srcacl too_many_requests sc_http_req_rate(0) gt 20http-request deny if too_many_requestsdefault_backend webservers
You also change the response code by setting the deny_status argument. Below, we return a 429 Too Many Requests error:
haproxyhttp-request deny deny_status 429 if too_many_requests
haproxyhttp-request deny deny_status 429 if too_many_requests
Tarpit an HTTP request Jump to heading
You can tarpit a client’s HTTP request, which stalls the request for a period of time before returning an error response. This is often used to deter a malicious bot army, since it ties up bots so that they cannot immediately retry their requests.
In the example below, we use http-request tarpit to impede the client if they exceed a rate limit. Use timeout tarpit to set how long the load balancer waits before returning an error response:
haproxyfrontend wwwbind :80# use a stick table to track request ratesstick-table type ip size 100k expire 1m store http_req_rate(1m)http-request track-sc0 srcacl too_many_requests sc_http_req_rate(0) gt 20timeout tarpit 10shttp-request tarpit deny_status 429 if too_many_requestsdefault_backend webservers
haproxyfrontend wwwbind :80# use a stick table to track request ratesstick-table type ip size 100k expire 1m store http_req_rate(1m)http-request track-sc0 srcacl too_many_requests sc_http_req_rate(0) gt 20timeout tarpit 10shttp-request tarpit deny_status 429 if too_many_requestsdefault_backend webservers
Pause an HTTP request or response Jump to heading
This section applies to:
- HAProxy 3.2 and newer
- HAProxy Enterprise 3.2 and newer
- HAProxy ALOHA 17.5 and newer
You can delay processing an HTTP request or response for a period of time using the pause argument with the http-request and http-response directives. The pause argument must be set with a timeout value (default is milliseconds) or an expression that returns a timeout value (where errors and invalid values are ignored).
Use cases include slowing down clients that exceed a rate limit or for debugging purposes.
haproxydefaults example1http-request pause 20 # pause request for 20 millisecondshttp-response pause 20 # pause response for 20 milliseconds...frontend example2http-request pause res.hdr(X-Pause-Seconds),mul(1000) # pause request for x secondshttp-response pause res.hdr(X-Pause-Seconds),mul(1000) # pause response for x seconds...backend example3http-request pause 20s # pause request for 20 seconds with "s" suffixhttp-response pause 20s # pause response for 20 seconds with "s" suffix...
haproxydefaults example1http-request pause 20 # pause request for 20 millisecondshttp-response pause 20 # pause response for 20 milliseconds...frontend example2http-request pause res.hdr(X-Pause-Seconds),mul(1000) # pause request for x secondshttp-response pause res.hdr(X-Pause-Seconds),mul(1000) # pause response for x seconds...backend example3http-request pause 20s # pause request for 20 seconds with "s" suffixhttp-response pause 20s # pause response for 20 seconds with "s" suffix...
Reject a TCP connection Jump to heading
You can reject a TCP connection, which closes the connection immediately without sending a response. The client’s browser will display the error message: The connection was reset. Use one of the following directives:
| Directive | Description |
|---|---|
http-request reject |
Closes the connection without a response after a session has been created and the HTTP parser has been initialized. Use this if you need to evaluate the request’s Layer 7 attributes (HTTP headers, cookies, URL). |
tcp-request content reject |
Closes the connection without a response once a session has been created, but before the HTTP parser has been initialized. These requests still show in your logs. |
tcp-request connection reject |
Closes the connection without a response at the earliest point, before a session has been created. These requests don’t show in your logs. |
In the following example, we reject connections originating from IP addresses we wish to block:
haproxyfrontend wwwbind :80acl banned_ip src -f /etc/hapee-3.3/blocklist.acltcp-request connection reject if banned_ipdefault_backend webservers
haproxyfrontend wwwbind :80acl banned_ip src -f /etc/hapee-3.3/blocklist.acltcp-request connection reject if banned_ipdefault_backend webservers
Silently replace the URL path Jump to heading
You can silently replace the URL path of an incoming client request before sending it to a backend server. The client is unaware of the replacement, and their browser address bar doesn’t update to reflect it. Use cases include replacing cookies, consolidating address schemes, stripping URL prefixes, or hiding directory structures.
In the following example, the /example-test/app/survey cookie will be removed from the URL path:
haproxyhttp-request replace-path (.*) /example-test/app/survey
haproxyhttp-request replace-path (.*) /example-test/app/survey
See the configuration manual’s replace-path reference for further details.
Silently drop a TCP connection Jump to heading
You can silently drop a client’s TCP connection, which disconnects immediately without notifying the client that the connection has been closed. This means that the load balancer frees any resources used for this connection. Clients will typically need to time out before they can release their end of the connection. Beware that silently dropping will affect any stateful firewalls or proxies between the load balancer and the client since they will hold onto the connection, unaware that it has been disconnected.
In the example below, we use http-request silent-drop to silently drop clients that access a restricted file:
haproxyfrontend wwwbind :80http-request silent-drop if { path_end /restricted.txt }default_backend webservers
haproxyfrontend wwwbind :80http-request silent-drop if { path_end /restricted.txt }default_backend webservers
Reject a QUIC connection Jump to heading
This section applies to:
- HAProxy 3.1 and newer
- HAProxy Enterprise 3.1 and newer
- HAProxy ALOHA 17.0 and newer
You can reject a QUIC connection, which closes the connection before the TLS handshake and sends a CONNECTION_REFUSED error code to the client. After failing to connect via HTTP/3 over QUIC, the client (browser) will typically fall back to using HTTP/2 over TCP. To block the client completely, you’ll also need to add rules that reject the TCP connection.
Below, we use quic-initial reject to reject connections originating from IP addresses we wish to block:
haproxyfrontend wwwbind :80bind :443 ssl crt /etc/haproxy/certs/example.com.pem ssl-min-ver TLSv1.3bind quic4@:443 ssl crt /etc/haproxy/certs/example.com.pem ssl-min-ver TLSv1.3http-response set-header alt-svc 'h3=":443"; ma=900'http-request redirect scheme https unless { ssl_fc }# Reject QUIC packets from banned IPsacl banned_ip src -f /etc/hapee-3.3/blocklist.aclquic-initial reject if banned_ipdefault_backend webservers
haproxyfrontend wwwbind :80bind :443 ssl crt /etc/haproxy/certs/example.com.pem ssl-min-ver TLSv1.3bind quic4@:443 ssl crt /etc/haproxy/certs/example.com.pem ssl-min-ver TLSv1.3http-response set-header alt-svc 'h3=":443"; ma=900'http-request redirect scheme https unless { ssl_fc }# Reject QUIC packets from banned IPsacl banned_ip src -f /etc/hapee-3.3/blocklist.aclquic-initial reject if banned_ipdefault_backend webservers
Silently ignore a QUIC connection Jump to heading
This section applies to:
- HAProxy 3.1 and newer
- HAProxy Enterprise 3.1 and newer
- HAProxy ALOHA 17.0 and newer
You can silently ignore a QUIC connection by using quic-initial dgram-drop. This ignores the reception of a QUIC Initial packet, preventing a QUIC connection in the first place.
haproxyfrontend wwwbind :80bind :443 ssl crt /etc/haproxy/certs/example.com.pem ssl-min-ver TLSv1.3bind quic4@:443 ssl crt /etc/haproxy/certs/example.com.pem ssl-min-ver TLSv1.3http-response set-header alt-svc 'h3=":443"; ma=900'http-request redirect scheme https unless { ssl_fc }# Reject QUIC packets from banned IPsacl banned_ip src -f /etc/hapee-3.3/blocklist.aclquic-initial dgram-drop if banned_ipdefault_backend webservers
haproxyfrontend wwwbind :80bind :443 ssl crt /etc/haproxy/certs/example.com.pem ssl-min-ver TLSv1.3bind quic4@:443 ssl crt /etc/haproxy/certs/example.com.pem ssl-min-ver TLSv1.3http-response set-header alt-svc 'h3=":443"; ma=900'http-request redirect scheme https unless { ssl_fc }# Reject QUIC packets from banned IPsacl banned_ip src -f /etc/hapee-3.3/blocklist.aclquic-initial dgram-drop if banned_ipdefault_backend webservers
See also Jump to heading
- Stick tables can be used for a variety of purposes. For more information, see Stick tables.
- To clear a stick table without interrupting the load balancer, use the clear table Runtime API command.
For complete information on directives and actions used in traffic policing, see these topics in the HAProxy Configuration Manual:
- For complete information about fetch methods, see Fetching samples reference.
- To deny a request, see http-request deny.
- To drop a request without responding, see http-request silent-drop.
- To delay a request, see http-request tarpit.
- To track sticky counters from a request, see http-request track.
- To halt processing and close a connection, see reject.
- To set the maximum number of connections, see server - maxconn.
- To define a stick table, see stick-table.
- To terminate a TCP connection, see tcp-request connection reject.
- To set the limit on time spent in the connection queue, see timeout queue.
- To set the tarpit delay time, see timeout tarpit.