Secure Cookies Using HAProxy Enterprise

My colleague Baptiste previously published an article on how to protect cookies while offloading SSL. I recently encountered a customer who wanted to achieve a very similar goal but using a more recent HAProxy Enterprise version. This post will explain the best practices for how to secure your cookies using HAProxy Enterprise.

How do Cookies Work?

HTTP is a stateless protocol meaning each new connection is completely independent from the previous one. The workaround for this is to use session cookies, enabling modern applications to allow long-running user sessions. This means once you are logged in you do not need to sign in for a period of time.

With cookies, information is sent from the server to the client using the set-cookie in the response header. The client (the web browser) sends it back to the server on the subsequent requests using the cookie request header. The server is now aware that the client is already known.

Cookies have many usages, most notably user authentication and settings. HAProxy can even be configured to use cookies to route clients among several backend servers. This ensures one client always gets routed to the same server and can be accomplished by enabling sticky sessions in HAProxy.

Web applications hosted over HTTPS are very common and cookies have to be secured in the same way. For that purpose, some attributes can be added to the set-cookie header. Here is a basic example:

Set-Cookie: User=Seb; path=/; Secure; HttpOnly

As you can see, cookie options are a semicolon delimited list. The flags can be defined in any order making their processing complex. Let’s have a closer look at the cookie:

  • User=Seb defines the cookie named User and its value Seb.

  • path=/ defines scope under which the cookie is considered to be valid.

  • Secure instructs the browser to consider this cookie be exclusively used over an HTTPS connection.

  • HttpOnly instructs the browser to use this cookie exclusively for HTTP. JavaScript code is denied to access this cookie.

(For more information about cookies and their attributes you can consult the Set-Cookie directive from RFC 6265.)

set-cookie header can be repeated several times. To add more complexity a set-cookie header may also be used to set many cookies. Please note that folding set-cookie into one header is not recommended (should not be used) by RFC 6265. It is tolerated in some cases but should be avoided in favor of having each cookie in its own header, we will see why later in this article. Below, I assume the application uses header folding. In this case, all cookies are sent using a comma delimited list. The following examples are equivalent:

set-cookie: Cookie1=Value1
set-cookie: Cookie2=Value-of-cookie2
set-cookie: Cookie3=Other-value; path=/
# Same result, but using header folding
set-cookie: Cookie1=Value1
set-cookie: Cookie2=Value-of-cookie2, Cookie3=Other-value; path=/

HAProxy Session Cookies

As mentioned, cookies can be used in HAProxy for session persistence in a backend by using both a cookie directive in the backend definition and a cookie value in the server definition.

backend webservers
[...]
cookie SRV insert indirect httponly secure
server s1 192.168.0.101:80 check cookie s1
server s2 192.168.0.102:80 check cookie s2

We use HAProxy as an SSL offloader and we want our session cookies to be secured both locally on the client and on the connection itself. That is why we add httponly secure in the backend’s cookie directive.

By default, cookies are accessible via the JavaScript API (such as the Document.cookie property). Adding the HttpOnly flag will deny their access to the JavaScript API and prevent XSS attacks.

When the client sends back the cookie to the server, and the connection is not encrypted, an attacker can dump the network traffic and collect sensitive data. Adding Secure instructs the browser to not send the cookie over an unencrypted connection.

Application Cookies

Our session cookie is now protected, however, the application behind the proxy may not be aware that the connection with the client is encrypted. The client may receive these headers, the first two of which define cookies sent from the application itself, while the third is the HAProxy controlled cookie that we secured:

set-cookie: Cookie1=Value1
set-cookie: Cookie2=Value-of-cookie2, Cookie3=Other-value; path=/
set-cookie: SRV=s1; path=/; HttpOnly; Secure

How can we protect these cookies?

Some applications can understand the x-forwarded-proto header and send secured cookies when it is set to https, but that requires the application to be compatible with this feature. We show an example of setting that header in our blog post Redirect HTTP to HTTPS with HAProxy.

With an older HAProxy version, we added the following snippet in the frontend definition to secure all cookies:

acl https ssl_fc
acl secured_cookie res.hdr(Set-Cookie),lower -m sub secure
rspirep ^(set-cookie:.*) \1;\ Secure if https !secured_cookie

But rspirep has been deprecated (HAProxy 2.0 is the latest to allow this syntax and will be discontinued in February 2024) and now the http-response replace-header action should be used instead. Many examples can be found on the web. Let’s try some of them. For the sake of these examples, let’s assume a few things:

  • We focus only on the HttpOnly attribute.

  • We want the frontend to add the attributes to all cookies (our session cookie is inserted using cookie SRV insert indirect, leaving off the secure attribute, because maybe we simply forgot to secure session cookies).

  • We want something generic that can be used everywhere without specifying the cookie name.

The application is configured to send the following cookies defined exactly as shown:

set-cookie: Cookie1=Value1, Cookie2=Value-of-cookie2; HttpOnly
set-cookie: Cookie3=Other-value; path=/

Yes, Cookie2 already has the HttpOnly attribute, but Cookie1 and Cookie3 do not.

1st Try

Here is how the frontend is configured to set the HttpOnly attribute:

acl http_cookie res.hdr(Set-Cookie),lower -m sub httponly
http-response replace-header Set-Cookie "(.*)" "\1; HttpOnly" if !http_cookie

Now let’s have a look at the response sent to the client:

set-cookie: Cookie1=Value1, Cookie2=Value-of-cookie2; HttpOnly
set-cookie: Cookie3=Other-value; path=/
set-cookie: SRV=s1; path=/

This configuration does not work, only Cookie1 is correctly defined. The problem comes from res.hdr which returns the value of the last entry (SRV=s1; path=/ in our example).

You may think SRV=s1; path=/ does not have HttpOnly attribute, and you are right. When used in an ACL res.hdr loops over all occurrences until a match is found. In other words, if one cookie has the HttpOnly attribute, we are unable to add it to other cookies.

2nd Try

http-response replace-header uses a regular expression to match the value to be replaced. If your HAProxy instance is compiled against PCRE (or PCRE2) regular expression libraries, you can benefit from the PCRE power. HAProxy Enterprise is compiled against the PCRE library. You can check if your HAProxy version is able to use PCRE with the following commands:

# Enterprise edition
/opt/hapee-2.6/sbin/hapee-lb -vv | grep 'Built with PCRE'
# Community edition
haproxy -vv | grep 'Built with PCRE'

You should see a line similar to

Built with PCRE2 version : 10.32 2018-09-10

Now the frontend should be defined as:

http-response replace-header Set-Cookie '(^((?!(?i)httponly).)*$)' "\1; HttpOnly"

The regular expression is a bit more complex than the previous one, let’s analyze it:

  • (?i)httponly matches the string httponly in a case insensitive manner. It matches httponly and also HttpOnly.

  • (?!(?i)httponly) inverts that match in a look ahead. It matches everything but httponly and its variants.

  • (^((?!(?i)httponly).)*$) matches at least one character in a string that does not contain httponly (and all its variants) and is captured into the \1 placeholder.

(For further information on PCRE you can check the official pcrepattern man page.)

You will also notice now that since we got rid of the ACL, we have a less complex configuration.

Please also note the single quotes around the regular expression. This prevents the dollar sign from being considered as a variable prefix. In HAProxy, a dollar sign within double quoted strings is a variable name prefix.

Let’s try it:

set-cookie: Cookie1=Value1, Cookie2=Value-of-cookie2; HttpOnly
set-cookie: Cookie3=Other-value; path=/; HttpOnly
set-cookie: SRV=s1; path=/

It’s better but this is not what we want. You can spot some issues: neither Cookie1 nor SRV are modified.

This is because replace-header will replace a whole header line no matter how many values are set.

3rd Try

Now let’s use replace-value instead of replace-header. It will act on all values, not only on a header line:

http-response replace-value Set-Cookie '(^((?!(?i)httponly).)*$)' "\1; HttpOnly"

Let’s check the result:

set-cookie: Cookie1=Value1; HttpOnly, Cookie2=Value-of-cookie2; HttpOnly
set-cookie: Cookie3=Other-value; path=/; HttpOnly
set-cookie: SRV=s1; path=/

We are almost there. All cookies now have a HttpOnly attribute but the SRV (the session affinity cookie). This cookie is a bit special because it is set by the proxy after the http-response rules are processed. We can fix it using http-after-response to modify proxy-generated headers.

Almost Final Version

Now the frontend setup is:

http-after-response replace-value Set-Cookie '(^((?!(?i)httponly).)*$)' "\1; HttpOnly"

Let’s check the result:

set-cookie: Cookie1=Value1; HttpOnly, Cookie2=Value-of-cookie2; HttpOnly
set-cookie: Cookie3=Other-value; path=/; HttpOnly
set-cookie: SRV=s1; path=/; HttpOnly

And voilà, this is what we want. But wait for it…

Final Version

Please note again that folding set-cookie headers, which lists multiple comma-separated cookies in a single header, should be avoided. A typical example is when an expires attribute comes in:

Set-Cookie: Cookie1=Value1; expires=Tue, 27-Sept-2023 09:14:05 GMT

The comma after Sun is considered a value delimiter and the replace-value will generate some invalid set-cookie header:

Set-Cookie: Cookie1=Value1; expires=Tue; HttpOnly, 27-Sept-2023 09:14:05 GMT; HttpOnly

In most cases, you want to use http-after-response replace-header action to secure your cookies:

http-after-response replace-header Set-Cookie '(^((?!(?i)httponly).)*$)' "\1; HttpOnly"

Conclusion

Usually, regular expressions should be avoided at all costs, especially case insensitive ones. They can become tedious to maintain and a real performance killer. In some other cases, it might be worth unleashing the full power of regular expressions to simplify the request processing logic.

Learn more about HAProxy Enterprise.

Full Proxy Setup

The full proxy setup is:

frontend www_fe
bind :80
bind :443 ssl crt my-cert.pem
mode http
use_backend www_be
http-after-response replace-header Set-Cookie '(^((?!(?i)httponly).)*$)' "\1; HttpOnly"
http-after-response replace-header Set-Cookie '(^((?!(?i)secure).)*$)' "\1; Secure" if { ssl_fc }
backend webservers
mode http
cookie SRV insert indirect
server s1 192.168.10.101:8000 check cookie s1
server s2 192.168.10.102:8000 check cookie s2
Subscribe to our blog. Get the latest release updates, tutorials, and deep-dives from HAProxy experts.