Let’s Encrypt certificates are designed to be short-lived, making manual renewal a constant chore.
HAProxy, a widely used TCP/HTTP load balancer, can be configured to automatically renew Let’s Encrypt certificates, ensuring your services remain accessible and secure without manual intervention. This process typically involves a combination of certbot (or a similar ACME client) and HAProxy’s configuration.
Here’s a typical setup demonstrating how this works:
Imagine you have a web service running behind HAProxy, and you want to secure it with Let’s Encrypt.
# /etc/haproxy/haproxy.cfg
frontend http_frontend
bind *:80
mode http
default_backend http_backend
frontend https_frontend
bind *:443 ssl crt /etc/letsencrypt/live/yourdomain.com/fullchain.pem
mode http
http-request redirect scheme https if !{ ssl_fc }
default_backend http_backend
backend http_backend
mode http
server webserver1 192.168.1.10:80 check
server webserver2 192.168.1.11:80 check
In this HAProxy configuration:
http_frontendlistens on port 80 and is used for the initial HTTP connection, often to redirect to HTTPS or to handle ACME challenges.https_frontendlistens on port 443, binds to the Let’s Encrypt certificate (/etc/letsencrypt/live/yourdomain.com/fullchain.pem), and redirects any non-SSL traffic to SSL.http_backenddefines your actual web servers.
The magic of automatic renewal happens with certbot. You’d typically set up certbot to run periodically using a cron job or systemd timer. The most common method for HAProxy is the standalone mode, where certbot temporarily runs its own web server to respond to Let’s Encrypt’s validation requests. However, for a production HAProxy setup, using the webroot plugin or, more elegantly, the dns plugin is preferred to avoid disrupting existing traffic.
Let’s focus on the webroot method for simplicity, assuming HAProxy is already serving your site on port 80/443. The webroot plugin tells certbot where your web server’s document root is, allowing it to place a validation file there.
First, ensure your HAProxy is configured to serve static files from a specific directory, which certbot can write to. A common approach is to have a dedicated alias or location in your HAProxy config that points to a directory accessible by certbot.
# /etc/haproxy/haproxy.cfg - snippet for webroot validation
frontend http_frontend
bind *:80
mode http
# ... other rules ...
# This location handles ACME challenges for certbot's webroot plugin
acl url_well_known_acme path_beg /.well-known/acme-challenge/
use_backend acme_challenge_backend if url_well_known_acme
backend acme_challenge_backend
mode http
# Point this to a directory certbot can write to.
# Ensure this directory is served by HAProxy.
# For example, if your webroot is /var/www/html, and certbot
# writes to /var/www/html/.well-known/acme-challenge/, HAProxy
# needs to be able to serve from there.
# A simpler approach for certbot's webroot is to let certbot
# manage files in a path that HAProxy *already* serves.
# So, if your main webroot is /var/www/html, and certbot
# places files in /var/www/html/.well-known/acme-challenge/,
# HAProxy *must* be configured to serve that path.
# The example above redirects all HTTP to HTTPS, so we might need
# to adjust the frontend to *not* redirect /.well-known/acme-challenge/
# or use a separate frontend/backend for it.
# A common pattern:
# frontend http_frontend
# bind *:80
# acl letsencrypt_challenge path_reg ^/\.well-known/acme-challenge/.*
# use_backend letsencrypt_backend if letsencrypt_challenge
# default_backend your_web_backend # For other HTTP traffic
# backend letsencrypt_backend
# mode http
# server letsencrypt_server 127.0.0.1:8080 # A dummy server or a path serving mechanism
# Let's assume a simpler scenario where HAProxy directly serves the webroot
# and certbot places files in it.
# The key is that HAProxy must serve the /.well-known/acme-challenge/ path.
# If your main webroot is /var/www/html, and certbot uses that,
# HAProxy must be configured to serve from /var/www/html.
# A common HAProxy config for serving static files:
# http-request set-path "%[capture.req.uri,regsub(^/path/to/serve,)]"
# server static_files 127.0.0.1:8080 # Or a direct file serving mechanism if HAProxy supports it.
# The most straightforward way is to ensure HAProxy's main frontend
# for port 80 is configured to serve the directory where certbot
# places its challenge files.
# Example: If your main web root is /var/www/html, and certbot uses it:
# frontend http_frontend
# bind *:80
# mode http
# acl acme_challenge_path path_beg /.well-known/acme-challenge/
# use_backend acme_challenge_backend if acme_challenge_path
# default_backend your_app_backend
# backend acme_challenge_backend
# mode http
# # If you're using a specific directory for challenges, e.g., /var/www/letsencrypt
# # and HAProxy is configured to serve static files from there.
# # This is complex. A simpler approach is to let certbot modify the *existing* webroot.
# # Let's assume your primary webroot is /var/www/html and HAProxy serves it.
# # Then certbot will place files in /var/www/html/.well-known/acme-challenge/
# # The HAProxy config must allow access to this path for HTTP.
# # If you redirect all HTTP to HTTPS, you need an exception for /.well-known/acme-challenge/
# # Example adjustment to the frontend:
# # frontend http_frontend
# # bind *:80
# # mode http
# # http-request deny if !{ path_beg /.well-known/acme-challenge/ } !{ ssl_fc } # Deny non-ACME HTTP traffic if not SSL
# # http-request redirect scheme https if { path_beg /.well-known/acme-challenge/ } !{ ssl_fc } # Or redirect ACME challenge too if your ACME client can handle it.
# # default_backend http_backend
# A more robust solution for HAProxy specifically uses the DNS challenge or
# a dedicated HTTP server for validation.
# For simplicity, let's assume certbot is configured to use the webroot
# and HAProxy is configured to serve that webroot. The critical part
# is that HAProxy *must* be able to serve /.well-known/acme-challenge/
# over HTTP *before* the certificate is renewed.
# After obtaining the certificate, HAProxy needs to be reloaded to pick up the new cert.
# This is typically done by a hook in certbot's renewal process.
The actual command to renew certificates and reload HAProxy would look something like this:
sudo certbot renew --deploy-hook "systemctl reload haproxy"
This command will:
certbot renew: Check all installed certificates and renew any that are close to expiring (typically within 30 days of expiry).--deploy-hook "systemctl reload haproxy": If a certificate is successfully renewed, execute the commandsystemctl reload haproxy. This tells HAProxy to gracefully reload its configuration, which includes picking up the newly updated certificate file.
The certbot process itself needs to be automated. A common way is to use cron:
# Add to root's crontab (sudo crontab -e)
0 3 * * * /usr/bin/certbot renew --deploy-hook "systemctl reload haproxy" >> /var/log/certbot-renew.log 2>&1
This cron job will run certbot renew every day at 3:00 AM. If any certificates are renewed, it will reload HAProxy. The output is logged to /var/log/certbot-renew.log.
The critical part of the webroot plugin is that HAProxy must be configured to serve the .well-known/acme-challenge/ directory. If your HAProxy configuration redirects all HTTP traffic to HTTPS, you’ll need to ensure this specific path is exempted from the redirect or handled by a separate mechanism. Without this, Let’s Encrypt’s validation will fail.
A more advanced approach, especially if you have a complex HAProxy setup or want to avoid modifying your main webroot, is to use certbot with the dns plugin. This involves creating a DNS TXT record to prove domain ownership. This method requires a script to interact with your DNS provider’s API.
The key to successful automatic renewal is ensuring the ACME client (certbot) can complete the validation challenge and that the web server (haproxy) is reloaded to use the new certificate file. The --deploy-hook is the bridge between these two steps.
Once you have automated renewal, the next hurdle is managing multiple domains and their respective certificates efficiently within HAProxy.