SSL Certificates for HAProxy, Caddy, and Other Web Servers

Most SSL guides focus on Nginx and Apache. But if you’re running HAProxy for load balancing, Caddy for automatic HTTPS, or Lighttpd on an embedded system, the configuration is different enough to warrant its own guide. I’ve set up SSL on all of these and the differences matter.

HAProxy: SSL Termination at the Load Balancer

HAProxy is often used as an SSL termination point — it handles HTTPS connections from clients and forwards plain HTTP (or re-encrypted HTTPS) to backend servers. This centralizes certificate management to a single place rather than every backend server.

Certificate Format for HAProxy

HAProxy needs a single PEM file that contains the private key, domain certificate, and certificate chain — in that order:

cat private.key certificate.crt ca-bundle.crt > combined.pem

That’s the opposite order from Nginx’s fullchain (where the cert comes first, then intermediates). HAProxy specifically wants the key first.

Basic SSL Termination Configuration

frontend https_frontend
    bind *:443 ssl crt /etc/haproxy/certs/combined.pem
    bind *:80
    http-request redirect scheme https unless { ssl_fc }
    default_backend web_servers

backend web_servers
    balance roundrobin
    server web1 192.168.1.10:80 check
    server web2 192.168.1.11:80 check

The ssl crt directive points to your combined PEM file. HAProxy reads the key, cert, and chain from that single file.

Multiple Certificates in HAProxy

For multiple domains, you can either use a directory of PEM files or specify them individually:

# Directory approach - HAProxy loads all .pem files in the directory
bind *:443 ssl crt /etc/haproxy/certs/

# Individual files
bind *:443 ssl crt /etc/haproxy/certs/site1.pem crt /etc/haproxy/certs/site2.pem

The directory approach is cleaner when managing many sites. HAProxy uses SNI to pick the right certificate for each domain.

TLS Settings in HAProxy

global
    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
    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
    ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets

ssl-default-bind-ciphers sets ciphers for TLS 1.2. ssl-default-bind-ciphersuites sets them for TLS 1.3 (different syntax). ssl-min-ver TLSv1.2 drops TLS 1.0 and 1.1 support.

SSL Health Check with HAProxy

Check that HAProxy is serving the correct certificate:

echo | openssl s_client -connect loadbalancer.example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates

Caddy: Automatic HTTPS With Zero Config

Caddy v2 is remarkable for SSL management. By default, it automatically provisions and renews Let’s Encrypt certificates for any domain it serves. There’s no separate Certbot, no cron jobs, no manual certificate management.

The Caddyfile: Minimal HTTPS Configuration

example.com {
    root * /var/www/example.com
    file_server
}

That’s the entire configuration. Caddy:

  1. Detects the domain name
  2. Obtains a Let’s Encrypt certificate via HTTP challenge
  3. Sets up HTTPS with modern TLS settings
  4. Redirects HTTP to HTTPS automatically
  5. Renews the certificate before it expires

If you access http://example.com, Caddy responds with a 301 redirect to https://example.com without any explicit configuration.

Using Custom Certificates in Caddy

If you have your own certificate (from a private CA, or a paid cert):

example.com {
    tls /etc/caddy/certs/certificate.crt /etc/caddy/certs/private.key
    root * /var/www/example.com
    file_server
}

For a PFX file:

example.com {
    tls {
        load /etc/caddy/certs/
    }
}

TLS Configuration in Caddy

Override the default TLS settings:

example.com {
    tls {
        protocols tls1.2 tls1.3
        ciphers TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        curves x25519 secp521r1 secp384r1 secp256r1
    }
}

Honestly, Caddy’s defaults are already excellent — modern ciphers, TLS 1.2+, OCSP stapling enabled. Most of the time you don’t need to configure TLS manually at all.

Caddy for Reverse Proxy with Automatic SSL

A very common Caddy use case:

example.com {
    reverse_proxy localhost:3000
}

api.example.com {
    reverse_proxy localhost:8080
}

Both subdomains get their own Let’s Encrypt certificates automatically. If you’re serving a Node.js app or any other HTTP service on a non-standard port, Caddy handles all the TLS in front of it. Each domain gets its own certificate and renewal cycle.

Lighttpd: SSL Configuration for Lightweight Servers

Lighttpd is less common than Nginx but appears in embedded systems, routers, and minimal server deployments. It requires mod_openssl for SSL.

server.modules += ("mod_openssl")

$SERVER["socket"] == ":443" {
    ssl.engine  = "enable"
    ssl.pemfile = "/etc/lighttpd/ssl/combined.pem"
    ssl.ca-file = "/etc/lighttpd/ssl/ca-bundle.crt"

    ssl.cipher-list = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:..."
    ssl.openssl.ssl-conf-cmd = ("MinProtocol" => "TLSv1.2")
}

Like HAProxy, Lighttpd wants the key and certificate combined in one file:

cat private.key certificate.crt > /etc/lighttpd/ssl/combined.pem

HTTP to HTTPS redirect in Lighttpd:

$HTTP["scheme"] == "http" {
    url.redirect = (".*" => "https://example.com$0")
}

Traefik: SSL for Docker Environments

Traefik is popular in Docker/Kubernetes environments. It integrates with Let’s Encrypt natively and reads configuration from container labels.

Static configuration (traefik.yml):

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web

Docker container label to enable automatic SSL:

# docker-compose.yml
services:
  myapp:
    image: myapp:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`example.com`)"
      - "traefik.http.routers.myapp.entrypoints=websecure"
      - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"

Traefik handles certificate issuance, renewal, and serving. Add a new container with the right labels and it gets HTTPS automatically.

Comparing SSL Certificate Management Across Servers

ServerCertificate FormatAuto-RenewalOCSP StaplingHTTP/2
NginxPEM (separate key/cert)No (use Certbot)Yes (manual config)Yes
ApachePEM (separate files)No (use Certbot)Yes (manual config)Via mod_http2
HAProxyCombined PEM (key first)No (use Certbot)Yes (built-in)Yes
CaddyAutomatic or custom PEMYes (built-in)Yes (automatic)Yes
LighttpdCombined PEM (key first)NoLimitedNo
TraefikAutomatic or custom PEMYes (built-in)Yes (automatic)Yes

For new projects where SSL management overhead matters, Caddy or Traefik are genuinely compelling options. The automatic certificate provisioning removes an entire category of operational work. For existing Nginx or Apache deployments, adding Certbot is the pragmatic path.

Scroll to Top