Skip to content

Letsencrypt integration with HAProxy and acme.sh

William Lallemand edited this page Jun 3, 2024 · 22 revisions

acme.sh integrates smoothly with HAProxy, one is able to start an haproxy configuration without any certificate, generate certificates with acme.sh and populate HAProxy with them. This will run acme.sh as non-root.

This tutorial uses a script which was not merged into acme.sh, if you have any issues, please report on the Pull Request https://github.com/acmesh-official/acme.sh/pull/4581

This procedure was written for ubuntu 22.04, but you can easily adapt it for other OSes.

HAProxy package installation

Please install the packages available on http://haproxy.debian.net, this way you will have an up to date version of HAProxy with the latest fixes.

root@ubuntu:~# sudo apt-get install --no-install-recommends software-properties-common
root@ubuntu:~# sudo add-apt-repository ppa:vbernat/haproxy-2.6
root@ubuntu:~# sudo apt-get install haproxy=2.6.\*
root@ubuntu:~# sudo apt-get install socat

acme.sh installation

Unfortunately acme.sh is not available as a package but its installation is easy. In this tutorial we will chose to run acme.sh with its own user which will have rights in the haproxy group in order to push certificates at the right place.

Create the acme user:

root@ubuntu:~# adduser --system --disabled-password --disabled-login --home /var/lib/acme --quiet --force-badname --group acme
root@ubuntu:~# adduser acme haproxy
root@ubuntu:~# mkdir /usr/local/share/acme.sh/

Install acme.sh in /usr/local/:

root@ubuntu:~# git clone https://github.com/acmesh-official/acme.sh.git
root@ubuntu:~# cd ./acme.sh
root@ubuntu:~# ./acme.sh --install --no-cron --no-profile --home /usr/local/share/acme.sh
root@ubuntu:~# ln -s /usr/local/share/acme.sh/acme.sh /usr/local/bin/
root@ubuntu:~# chmod 755 /usr/local/share/acme.sh/

Generate your ACME account

Generate your acme account from the acme user

root@ubuntu:~# sudo -u acme -s
acme@ubuntu:~$ acme.sh --register-account
[Mon Apr 24 01:28:14 PM UTC 2023] Create account key ok.
[Mon Apr 24 01:28:14 PM UTC 2023] Registering account: https://ACMESERVER/acme/acme/directory
[Mon Apr 24 01:28:14 PM UTC 2023] Registered
[Mon Apr 24 01:28:14 PM UTC 2023] ACCOUNT_THUMBPRINT='lCufto4sDRTHdmWL0EugFywGV54hBCuTTXvwifi65R4'

Write the thumbprint somewhere, it will be used to configure HAProxy.

Configure HAProxy

acme.sh is able to run in stateless mode, which means it will only be used as an ACME client, but the HTTP challenge will be delivered by a third-party, in our case this will be HAProxy. In order to achieve this, HAProxy needs to return a specific value.

Create the directory for certificates:

root@ubuntu:~# mkdir /etc/haproxy/certs
root@ubuntu:~# chown haproxy:haproxy /etc/haproxy/certs
root@ubuntu:~# chmod 770 /etc/haproxy/certs

Then edit /etc/haproxy/haproxy.cfg to add the challenge response and the thumbprint. Don't forget the stats socket with correct permissions which are needed for the deployment script:

global
    stats socket /var/run/haproxy/admin.sock level admin mode 660
    setenv ACCOUNT_THUMBPRINT 'lCufto4sDRTHdmWL0EugFywGV54hBCuTTXvwifi65R4'

frontend web
    bind :80
    bind :443 ssl crt /etc/haproxy/certs/ strict-sni
    http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_beg '/.well-known/acme-challenge/' }

You can then start HAProxy with the empty directory, the strict-sni keyword allows it.

Generate a certificate

At this step you can generate a certificate with acme.sh, and haproxy will respond to the challenge, you should be able to see it in your haproxy logs. Run the command as the acme user:

root@ubuntu:~# sudo -u acme -s
acme@ubuntu2204:~$ acme.sh --issue -d domain1.com --stateless
[Mon Apr 24 01:36:03 PM UTC 2023] Using CA: https://ACMESERVER/acme/acme/directory
[Mon Apr 24 01:36:03 PM UTC 2023] Single domain='domain1.com'
[Mon Apr 24 01:36:03 PM UTC 2023] Getting domain auth token for each domain
[Mon Apr 24 01:36:04 PM UTC 2023] Getting webroot for domain='domain1.com'
[Mon Apr 24 01:36:04 PM UTC 2023] Verifying: domain1.com
[Mon Apr 24 01:36:04 PM UTC 2023] Stateless mode for domain:domain1.com
[Mon Apr 24 01:36:06 PM UTC 2023] Success
[Mon Apr 24 01:36:06 PM UTC 2023] Verify finished, start to sign.
[Mon Apr 24 01:36:06 PM UTC 2023] Lets finalize the order.
[Mon Apr 24 01:36:06 PM UTC 2023] Le_OrderFinalize='https://ACMESERVER/acme/acme/order/VTf79HKx9LeAqKIn3JVLfAQHE20G5o72/finalize'
[Mon Apr 24 01:36:06 PM UTC 2023] Downloading cert.
[Mon Apr 24 01:36:06 PM UTC 2023] Le_LinkCert='https://ACMESERVER/acme/acme/certificate/2neCjpZPEWa1wkbxOQjcFo3R0Xm1XJeX'
[Mon Apr 24 01:36:06 PM UTC 2023] Cert success.
-----BEGIN CERTIFICATE-----
[...] 
-----END CERTIFICATE-----
[Mon Apr 24 01:36:06 PM UTC 2023] Your cert is in: /var/lib/acme/.acme.sh/domain1.com_ecc/domain1.cer
[Mon Apr 24 01:36:06 PM UTC 2023] Your cert key is in: /var/lib/acme/.acme.sh/domain1.com_ecc/domain1.key
[Mon Apr 24 01:36:06 PM UTC 2023] The intermediate CA cert is in: /var/lib/acme/.acme.sh/domain1.com_ecc/ca.cer
[Mon Apr 24 01:36:06 PM UTC 2023] And the full chain certs is there: /var/lib/acme/.acme.sh/domain1.com_ecc/fullchain.cer

Certificate deployment

Once the certificate is generated you can deploy it in the certs directory and to haproxy without reload it, with the stats socket. You must set the DEPLOY_HAPROXY_STATS_SOCKET and DEPLOY_HAPROXY_PEM_PATH with the rights value corresponding to the path of the haproxy socket and the directory which will store the certificates. It is also possible to use the 'DEPLOY_HAPROXY_MASTER_CLI' to use the master CLI instead of a stats socket. Be careful not to add any trailing slash in DEPLOY_HAPROXY_PEM_PATH as it won't be able to match the haproxy configuration.

root@ubuntu:~# sudo -u acme -s
acme@ubuntu:~$ DEPLOY_HAPROXY_HOT_UPDATE=yes DEPLOY_HAPROXY_STATS_SOCKET=UNIX:/var/run/haproxy/admin.sock DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy/certs acme.sh --deploy -d domain1.com --deploy-hook haproxy
[Mon Apr 24 02:17:31 PM UTC 2023] The domain 'domain1.com' seems to have a ECC cert already, lets use ecc cert.
[Mon Apr 24 02:17:31 PM UTC 2023] Deploying PEM file
[Mon Apr 24 02:17:31 PM UTC 2023] Moving new certificate into place
[Mon Apr 24 02:17:31 PM UTC 2023] Creating new certificate '/etc/haproxy/certs/domain1.com.pem' over HAProxy stats socket.
[Mon Apr 24 02:17:31 PM UTC 2023] Success

This command will deploy the certificate to /etc/haproxy/certs and inject the certificate over haproxy stats socket.

Once it succeed you can check if the certificate was added in HAProxy with socat:

root@ubuntu:~# echo "show ssl cert /etc/haproxy/certs/domain1.com.pem" | socat /var/run/haproxy/admin.sock -
Filename: /etc/haproxy/certs/domain1.com.pem
Status: Used
Serial: 6C058BA29A03DF58BFA9CF0ABD0C0AB8
notBefore: Apr 24 13:35:04 2023 GMT
notAfter: Apr 25 13:36:04 2023 GMT
Subject Alternative Name: DNS:domain1.com
Algorithm: EC256
SHA1 FingerPrint: 7A6A64B2D3BB1E548DBF383FDB3A7CA8434B3F5A
[...]

And then directly with curl:

$ curl https://domain1.com

Certificate renewal

The certificates can be renewed and updated automatically using the --cron option. It is recommended to use the crontab of the acme user or a systemd timer.

Systemd Timer

Inspired from https://github.com/acmesh-official/acme.sh/wiki/Using-systemd-units-instead-of-cron

Create a new systemd service in /etc/systemd/system/acme_letsencrypt.service:

[Unit]
Description=Renew Let's Encrypt certificates using acme.sh
After=network-online.target

[Service]
Type=oneshot
# --home's argument should be where the acme.sh script resides.
ExecStart=/usr/local/bin/acme.sh --cron
User=acme
Group=acme

SuccessExitStatus=0 2

Test the service:

root@ubuntu:~# systemctl daemon-reload
root@ubuntu:~# systemctl start acme_letsencrypt

Create a time in /etc/systemd/system/acme_letsencrypt.timer

[Unit]
Description=Daily renewal of Let's Encrypt's certificates

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

Enable the timer:

root@ubuntu:~# systemctl start acme_letsencrypt.timer
root@ubuntu:~# systemctl enable acme_letsencrypt.timer

Crontab

Using the crontab is easy since this is integrated in acme.sh:

root@ubuntu:~# sudo -u acme -s
acme@ubuntu:~$ acme.sh --install-cronjob