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.
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:
As you can see, cookie options is 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=Sebdefines the cookie named
Userand its value
path=/defines scope under which the cookie is considered to be valid.
Secureinstructs the browser to consider this cookie be exclusively used over an HTTPS connection.
(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:
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.
We use HAProxy as a 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
Document.cookie property). Adding the
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.
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:
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:
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
- We want the frontend to add the attributes to all cookies (our session cookie is inserted using
cookie SRV insert indirect, leaving off the
secureattribute, 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:
Cookie2 already has the
HttpOnly attribute, but
Cookie3 do not.
Here is how the frontend is configured to set the
Now let’s have a look at the response sent to the client:
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.
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 following commands:
You should see a line similar to
Now the frontend should be defined as:
The regular expression is a bit more complex than the previous one, let’s analyze it:
(?i)httponlymatches the string
httponlyin a case insensitive manner. It matches
(?!(?i)httponly)inverts that match in a look ahead. It matches everything but
httponlyand its variants.
(^((?!(?i)httponly).)*$)matches at least one character in a string which does not contain
httponly(and all its variants) and is captured into the
(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:
It’s better but this is not what we want. You can spot some issues: neither
SRV are modified.
This is because
replace-header will replace a whole header line no matter how many values are set.
Now let’s use
replace-value instead of
replace-header. It will act on all values, not only on a header line:
Let’s check the result:
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:
Let’s check the result:
And voilà, this is what we want. But wait for it…
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:
The comma after
Sun is considered as a value delimiter and the
replace-value will generate some invalid
In most cases you want to use
http-after-response replace-header action to secure your cookies:
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: