Configuring TLS with haproxy¶
Vinyl Cache famously does not directly support TLS (SSL) like many other http servers, because we believe that sensitive key material should not be kept in the address space of a program processing internet traffic. See Why no SSL? and SSL revisited for details.
As of April 2026, unpublished work is ongoing to change this and implement TLS support with a separate key server (which will be called TLSentinel, so you have a reference for future announcements). This work will be published as real, honest (no-corporate-BS) FOSS later in 2026.
But in the meantime, we document here the setup which has been used in all kinds of Vinyl Cache deployments since 2019, and which comes close to the ideal setup which we ultimately want to get to: Use haproxy to provide frontend and backend TLS support for Vinyl Cache with minimal configuration overhead.
With this setup, the process which encrypts and decrypts traffic (haproxy) still also holds the key material, but it only handles streams of data and otherwise does no complex processing of HTTP - this all happens in Vinyl Cache.
The TLSandwich¶
The big picture of this setup looks a bit like a sandwich, except that the bread on the top and bottom is, in the simplest of configurations, the same thing:
users
| TLS HTTP/1.1 and/or HTTP/2 over TCP
V
+-------------------+
| haproxy OFFLOAD |
| +----------------+
| | |
| | | clear HTTP over UDS with PROXY preamble
| | V
| | +-------------+
| | | Vinyl Cache |
| | +-------------+
| | |
| | | clear HTTP over UDS with PROXY preamble
| | V
| +----------------+
| haproxy ONLOAD |
+-------------------+
|
| TLS HTTP/1.1 over TCP
V
backends
Note
This setup is exemplary only, and many variations are possible: More than one haproxy instance can be used, and UDS can be replaced with TCP. Also, haproxy can terminate HTTP/3 as well, but then tcp mode can not be used.
How the sandwich works¶
The TL;DR is that, in this setup, haproxy performs
the TLS offload function to turn encrypted user traffic into clear HTTP and
the TLS onload function to turn clear backend HTTP traffic from Vinyl Cache into TLS traffic to backends.
In a bit more detail, this works as follows:
haproxy terminates TLS for users, it manages all the TLS certificates, accepts connections, encrypts/de-crypts traffic and sends the clear traffic onwards to Vinyl Cache.
The connection to Vinyl Cache can use a Unix Domain Socket, which reduces the attack surface and improves performance if both haproxy and Vinyl Cache run on the same kernel. Note that this also works with multiple containers in the same pod, that is, in containerized environments like kubernetes, isolation can be extended even beyond the process level.
At the start of the connection to Vinyl Cache, haproxy sends a small PROXY preamble, which contains relevant socket and TLS information, like the client IP address, the used TLS crtificate, the used algorithms etc.
Thanks to the PROXY protocol, handling an offloaded TLS connection in Vinyl Cache behaves almost exactly as if it was terminated directly. For all practical purposes, one can forget about this detail when writing VCL once a basic setup is in place.
For backend connections via TLS, Vinyl Cache connectios to haproxy, again via UDS, and again using the PROXY protocol, but this time it is Vinyl Cache sending a PROXY preamle: It contains the IP address and port to connect to. haproxy performs the connection including all TLS and, again, presents the clear traffic to Vinyl Cache and the encrypted traffic to the backend.
Thanks to haproxy, this clever use of the PROXY protocol enables Vinyl Cache manage TLS enabled backends like non-TLS backends without implementing the actual TLS. This really deserves a big shout out to Willy Tarreau for implementing this smart idea in haproxy and then telling us about it when we asked.
Show me the code!¶
This section conains examplary configurations for the setup introduced above with HTTP/1.1 support only.
Vinyl Cache¶
For the example configuration, we start vinyld with:
-a uds_proxy=/var/lib/haproxy/tlsoff.sock,PROXY,mode=770
This expects vinyld and haproxy to have vinyl as their primary
group.
haproxy¶
Here’s a haproxy.cfg as a starting point, taken from an actual
production system with only minor edits:
global
maxconn 512000
master-worker
mworker-max-reloads 3
nbthread 10
# mapped to the first 10 CPU cores
cpu-map auto:1/1-10 0-9
user nobody
group vinyl
daemon
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-dh-param-file /etc/haproxy/dhparam
tune.ssl.cachesize 200000
tune.ssl.lifetime 900
hard-stop-after 29m
defaults
log global
mode tcp
option dontlognull
retries 3
option redispatch
maxconn 512000
backlog 8192
timeout connect 15000ms
timeout client 300000ms
timeout server 300000ms
timeout client-fin 1s
timeout server-fin 1s
timeout queue 1m
timeout tunnel 1h
listen tlsoff
bind *:443 ssl crt /etc/ssl/chains no-sslv3
server vinyl /var/lib/haproxy/tlsoff.sock send-proxy-v2
listen tlson
mode tcp
maxconn 1000
bind /var/lib/haproxy/tlson.sock accept-proxy mode 770
balance roundrobin
stick-table type ip size 100
stick on dst
server s01 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s02 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s03 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s04 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s05 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s06 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s07 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s08 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s09 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s10 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s11 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s12 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s13 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s14 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s15 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s16 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
#...approximately as many servers as expected peers for improved tls session caching
Attention
The fact that this configuration is running live does not mean it is perfect. Think about it when you copy and paste.
The relevant parts of this configuration are:
listen tlsoffconfigures the TLS offloader (the top part of the sandwich) to forward all traffic to/var/lib/haproxy/tlsoff.sock, accepting all certificates in/etc/ssl/chains.listen tlsonconfigures the TLS onloader (the bottom part of the sandwich) to accept PROXY connections fromvinyldand forward them to the server in the PROXY preamble. For these connections, any certificate in/etc/ssl/certs/ca-bundle.crtis accepted.
Besides the usual suspects like maxconn and the fact that you will probably
want to adjust the cpu-map, nbthread and tune.ssl.* to your system
environment, one notable detail are the many server directives in listen
tlson. These should roughly match the expected number of peers used for
backend servers.
Certificate management with the example setup¶
With this example configuration, certificate management is straight forward:
Put all server certificates in “chain” format (private key, then full certificate chain) in
/etc/ssl/chains. Reload haproxy when changing any files in this directory.Put all certificates to accept for backend connections in
/etc/ssl/certs/ca-bundle.crt
Managing client connections in VCL¶
In VCL, the original client IP address connecting to haproxy is available as
client.ip. To identify TLS connections, std.port(server.ip) can be
checked, for example:
import std;
sub vcl_recv {
if (std.port(server.ip) == 443) {
set req.http.X-Forwarded-Proto = "https";
}
}
For additional information about the TLS connection attributes, the bundled vmod_proxy can be used. See there for examples.
Using backend TLS connections in VCL¶
Making TLS connections is a little more involved, but not much: We need to
tell Vinyl Cache where to find haproxy’s tlson socket and to not make the
connection directy, but rather through it, using the PROXY protocol. The
abstraction for such connections are via backends, where we instruct one
backend to make a connection through another:
backend tlson {
.path = "/var/lib/haproxy/tlson.sock";
}
backend example {
.host = "www.example.com";
.port = "443";
.via = tlson;
}
Note that if .host is an IP address, the .host_header or .authority
attributes also need to be set for servers requiring SNI, which basically all
servers do these days. See the via documentation for details.
Dynamic backends via TLS¶
Native VCL backends as configured using the backend statement in VCL are
limited to a single IP address, which remains constant while a VCL is active.
This behavior is very useful for simple, explicit configurations, but nowadays
many envirements (do I hear anyone saying “cloud”?) require backend servers to
be
discovered through frequently changing DNS A, AAAA or SRV records.
In Vinyl Cache, backends which change during the lifetime of a VCL are called dynamic backends, and one populat module to implement them is vmod_dynamic.
To make TLS connections with this module, we define a director to use the onloader provided by haproxy:
sub vcl_init {
new https = dynamic.director(via = tlson, port = 443);
}
Then making TLS connections to arbitrary hosts is as simple as:
sub vcl_backend_fetch {
set bereq.http.Host = "example.com";
set bereq.backend = https.backend();
}
HTTP/2¶
To also offer HTTP/2 for client connections, the following two changes need to be made to the example configuration:
vinyldneeds to be started with-p feature=+http2The
listen tlsoffconfiguration of haproxy needs to havealpn h2,http/1.1added:listen tlsoff bind *:443 ssl crt /etc/ssl/chains no-sslv3 alpn h2,http/1.1 server vinyl /var/lib/haproxy/tlsoff.sock send-proxy-v2
Abstract Sockets¶
Where abstract sockets are supported (e.g. on Linux), the UDS paths can be
replaced with abstract socket names, which further simplifies the configuration
by avoiding the permission checks (mode bits, user/group ownership) coming with
unix domain sockets. For example, @tlsoff can be used instead of
/var/lib/haproxy/tlsoff.sock. Note that the mode settings have no
meaning for abstract sockets.
Conclusion¶
While we work on further improvements, the setup introduced herein provides a low-maintenance, “configure and forget” solution for using TLS for client- and backend traffci with Vinyl Cache.
Contributing¶
We welcome fixes and improvements to this tutorial! To contribute, plase go to https://code.vinyl-cache.org/vinyl-cache/homepage
For how to get an account, please see https://code.vinyl-cache.org/vinyl-cache/code.vinyl-cache.org#invitations and https://vinyl-cache.org/organization/moving.html