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 tlsoff configures 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 tlson configures the TLS onloader (the bottom part of the sandwich) to accept PROXY connections from vinyld and forward them to the server in the PROXY preamble. For these connections, any certificate in /etc/ssl/certs/ca-bundle.crt is 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:

  • vinyld needs to be started with -p feature=+http2

  • The listen tlsoff configuration of haproxy needs to have alpn h2,http/1.1 added:

    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