Skip to content

Conversation

@wlallemand
Copy link
Contributor

@wlallemand wlallemand commented Apr 4, 2023

Since version 2.2, HAProxy is able to update dynamically certificates, without a reload.

This patch uses socat to push the certificate into HAProxy in order to achieve hot update. With this method, reloading is not required. This should be used only to update an existing certificate in haproxy.

2 new variables are available:

  • DEPLOY_HAPROXY_HOT_UPDATE="yes" update over the stats socket instead of reloading

  • DEPLOY_HAPROXY_STATS_SOCKET="UNIX:/run/haproxy/admin.sock" set the path on the stats socket.

@wlallemand
Copy link
Contributor Author

I just updated the PR, it now has the ability to add a new certificate in HAProxy without reloading as well as updating a previous one.

@msimerson
Copy link

msimerson commented May 12, 2023

Hey @wlallemand, this is AWESOME, thank you. I was getting ready to write this functionality and decided I should check if it already exists. I ran into a few issues I had to address to get it working for me.

  1. my fullchain.pem files have an extra \n in them. Apparently the chain cert is coming back that way from Let's Encrypt. Because <<\n$(cat $path)\n (as shown in haproxy docs) depends on \n as a boundary, the payload fails to send to haproxy correctly. The solution is simple: replace cat with grep . $path
  2. I communicate with haproxy admin API via TCP port instead of sock. This is easy to accommodate by moving the UNIX: into _statssock, so it can be set to tcp-connect:192.168.0.1:9000. Probably best to include an example of each as comments.
  3. my haproxy is chroot, so the acme.sh deploy path and the haproxy path to same file are different
    • I store the haproxy installed path in _ipem.

@wlallemand
Copy link
Contributor Author

wlallemand commented May 15, 2023

@msimerson Thanks for your feedback, I'm going to fix the script to integrate properly your use case.

1. my fullchain.pem files have an extra `\n` in them. Apparently the chain cert is coming back that way from Let's Encrypt. Because `<<\n$(cat $path)\n` (as shown in [haproxy docs](https://www.haproxy.com/blog/dynamic-ssl-certificate-storage-in-haproxy/)) depends on `\n` as a boundary, the payload fails to send to haproxy correctly. The solution is simple: replace cat with `grep . $path`

Indeed. I did not had any problem with the provider I used but that's a valid point, it could contains empty lines or comments between the certificate sections.

2. I communicate with haproxy admin API via TCP port instead of sock. This is easy to accommodate by moving the UNIX: into `_statssock`, so it can be set to `tcp-connect:192.168.0.1:9000`. Probably best to include an example of each as comments.

I hesitate to include directly the socat format into the variable, maybe I could add a variable to be able to chose any socat prefix.

3. my haproxy is chroot, so the acme.sh deploy path and the haproxy path to same file are different
   
   * I store the haproxy installed path in _ipem.

Indeed that could be a problem, I will retest all of this.

Also, I published a documentation on the haproxy wiki: https://github.com/haproxy/wiki/wiki/Letsencrypt-integration-with-HAProxy-and-acme.sh

@msimerson
Copy link

I hesitate to include directly the socat format into the variable, maybe I could add a variable to be able to chose any socat prefix.

I'm curious how you think that having 2 socat variables is going to be easier for folks to grok and easier to support than having just one. It's not like the variable contents will ever be used separately. URIs evolved for a reason. :-)

@wlallemand
Copy link
Contributor Author

@msimerson problem is socat and haproxy doesn't have the same "URIs". I don't want the user to need to specify a socat format. Maybe I'll just check if the variable contains any / to select UNIX-CONNECT or TCP-CONNECT.

@msimerson
Copy link

@msimerson problem is socat and haproxy doesn't have the same "URIs". I don't want the user to need to specify a socat format. Maybe I'll just check if the variable contains any / to select UNIX-CONNECT or TCP-CONNECT.

I see. In looking again, you use _statssock (3) times without the UNIX: prefix and (3) times with. If you were consistent and used it only without the prefix, then I think you get exactly what you want (user doesn't need to specify), and it'll work as you expect, because socat will assume the right thing:

Address specifications starting with a number are assumed to be FD (raw file descriptor) addresses; if a ’/’ is found before the first ’:’ or ’,’, GOPEN (generic file open) is assumed.

And anyone needing a different socat formula can specify it.

@wlallemand
Copy link
Contributor Author

I see. In looking again, you use _statssock (3) times without the UNIX: prefix and (3) times with. If you were consistent and used it only without the prefix, then I think you get exactly what you want (user doesn't need to specify), and it'll work as you expect, because socat will assume the right thing:

Indeed, my mistake, I should also fix this. My first implementation was a little bit different and I probably forgot it.

Address specifications starting with a number are assumed to be FD (raw file descriptor) addresses; if a ’/’ is found before the first ’:’ or ’,’, GOPEN (generic file open) is assumed.

I want to avoid this and find it dangerous, if for some reason the path is wrong and is not a UNIX socket, it would do a GOPEN and the certificate could be dumped on the filesystem. That's exactly why I set UNIX: in the first place.

And anyone needing a different socat formula can specify it.

That's why I suggested another variable, but to be honest there is not a lot of common setup. It's basically TCP with ipv4 and ipv6 and unix socket, some people could also want to use OPENSSL... I think I'll remove the prefix and adjust the documentation as you suggested in the first place, with a warning about not using a prefix.

@msimerson
Copy link

it would do a GOPEN and the certificate could be dumped on the filesystem

In this case, the certificate is already on the filesystem, right? If you're super worried about it, you can always do a -S test on the path (after determining it's a path, by checking that the first character is a /).

@iarenaza
Copy link

3. my haproxy is chroot, so the acme.sh deploy path and the haproxy path to same file are different
   
   * I store the haproxy installed path in _ipem.

Just wanted to chime in to say that we are not using chroot for haproxy, but running it inside a container. And pretty much the same situation arises there: paths outside the container (where acme.sh stores the certs) and inside the container (where haproxy looks for them) don't match.

Is this something that could be supported?

@wlallemand
Copy link
Contributor Author

Thanks for the feedbacks, I'll rework the PR next week to better handle these cases.

@dexter-dopping-ekco
Copy link

Hi folks,

To get the script to work right on my setup (Debian 12, HaProxy 2.6.12-1) I had to make a small tweak.

It seems like the stats socket doesn't read the entire input when passing in the file contents, instead stopping after reading an empty line, which means it doesn't read the CA certificate chain. This is what it looks like when running the deploy command with --debug 2:

...
-----END CERTIFICATE-----\n' | socat 'UNIX:/var/run/haproxy/admin.sock' - | grep -q 'Transaction created''
2023/11/24 20:11:20 socat[118250] E write(1, 0x5588acdfe000, 8192): Broken pipe
[Fri 24 Nov 2023 20:11:20 GMT] _socat_cert_commit_cmd='echo 'commit ssl cert /etc/haproxy/certs/example.com.pem' | socat 'UNIX:/var/run/haproxy/admin.sock' - | grep -q '^Success!$''
[Fri 24 Nov 2023 20:11:20 GMT] Success

The deploy script logs it as successful, but socat wasn't able to write everything (Broken pipe). openssl s_client shows that the certificate chain is incomplete:

dexter@dexter-dell:~$ openssl s_client -connect example.com:443 -servername example.com </dev/null
CONNECTED(00000003)
depth=0 CN = example.com
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = example.com
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 CN = example.com
verify return:1
---
Certificate chain
 0 s:CN = example.com
   i:C = US, O = Let's Encrypt, CN = R3
--- ...

#4788 mentions the same "Broken pipe" message but the related MR #4841 did not help and even seems to have made things worse, since the key no longers get imported when it's moved to the end of the PEM file.

Only after removing the empty lines does command with hot update work correctly for me.

This is the change I made:

diff --git a/haproxy-wlallemand.sh b/usr/local/share/acme.sh/deploy/haproxy.sh
index a611ec8..bb2b6b1 100644
--- a/haproxy-wlallemand.sh
+++ b/usr/local/share/acme.sh/deploy/haproxy.sh
@@ -177,7 +177,7 @@ haproxy_deploy() {
   # Create a temporary PEM file
   _temppem="$(_mktemp)"
   _debug _temppem "${_temppem}"
-  cat "${_ckey}" "${_ccert}" "${_cca}" >"${_temppem}"
+  cat "${_ckey}" "${_ccert}" "${_cca}" | grep . >"${_temppem}"
   _ret="$?"

   # Check that we could create the temporary file

Now the deploy command runs fine and the openssl s_client command shows the complete chain:

dexter@dexter-dell:~$ openssl s_client -connect example.com:443 -servername example.com </dev/null
CONNECTED(00000003)
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = R3
verify return:1
depth=0 CN = example.com
verify return:1
---
Certificate chain
 0 s:CN = example.com
   i:C = US, O = Let's Encrypt, CN = R3
 1 s:C = US, O = Let's Encrypt, CN = R3
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
 2 s:C = US, O = Internet Security Research Group, CN = ISRG Root X1
   i:O = Digital Signature Trust Co., CN = DST Root CA X3
--- ...

wlallemand and others added 3 commits November 30, 2023 14:00
Sanitize the PEM of the haproxy deploy script by removing the '\n', this
way it could be injected directly over the CLI.
Since version 2.2, HAProxy is able to update dynamically certificates,
without a reload.

This patch uses socat to push the certificate into HAProxy in order to
achieve hot update. With this method, reloading is not required.
This should be used only to update an existing certificate in haproxy.

2 new variables are available:

- DEPLOY_HAPROXY_HOT_UPDATE="yes" update over the stats socket instead
  of reloading

- DEPLOY_HAPROXY_STATS_SOCKET="UNIX:/run/haproxy/admin.sock" set the path on
  the stats socket.
DEPLOY_HAPROXY_HOT_UPDATE="yes" now allows to add a new certificate
within HAProxy instead of updating an existing one.

In order to work, the ${DEPLOY_HAPROXY_PEM_PATH} value must be used as a
parameter to the "crt" keyword in the haproxy configuration.

The patch uses the following commands over HAProxy stats socket:
- show ssl cert
- new ssl cert
- set ssl cert
- commit ssl cert
- add ssl crt-list
@wlallemand
Copy link
Contributor Author

wlallemand commented Nov 30, 2023

Small updates:

  • sanitize the PEM to remove the '\n'
  • shellcheck fixes
  • socat format is directly used in the DEPLOY_HAPROXY_STATS_SOCKET variable

@Neilpang Any chance this can be merged? This is actually asked by a lot of people.

@wlallemand
Copy link
Contributor Author

I'll make more updates for chroot and wilcard as well as the master CLI instead of the stats socket, but probably in a separate PR.

haproxy-mirror pushed a commit to haproxy/haproxy that referenced this pull request Nov 30, 2023
acmesh-official/acme.sh#4581 was updated, this
patch update the haproxy repository with the update.
the following changes were done:

- sanitize the PEM to remove the '\n' (truncated certicate chain)
- shellcheck fixes
- socat format is directly used in the DEPLOY_HAPROXY_STATS_SOCKET variable
DEPLOY_HAPROXY_MASTER_CLI allows to use the HAProxy master CLI instead
of a stats socket for DEPLOY_HAPROXY_HOT_UPDATE="yes"

The syntax of the master CLI is slightly different, a prefix with the
process number need to be added before any command.

This patch uses ${_cmdpfx} in front of every socat commands which is
filled when the master CLI is used.
By default acme.sh uses the '*' character in the filename for wildcard.
That can be confusing within HAProxy since the * character in front of a
filename in the stat socket is used to specified an uncommitted
transaction.

This patch replace the '*' by a '_' in the filename.
This is only done when using the default filename, the name can still be
forced with an asterisk.
@wlallemand
Copy link
Contributor Author

update:

  • master CLI support was implemented
  • wilcard character is replaced in the filename

Copy link

@dingensundso dingensundso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in #4581 (comment) I'm not sure about replacing the asterisk with an underscore, since some DNS servers allow underscore as a subdomain even though RFC1035 doesn't allow it.
Let's Encrypt for one doesn't issue certificates for underscore domains but I don't know about other acme server implementations.

Would love to see input from other people about this.

Otherwise I don't see any problems with the script.

@krezovic
Copy link

krezovic commented Dec 19, 2023

Hi @wlallemand. I just stumbled upon this trying to configure OCSP Stapling in haproxy. Is it possible to use Le_OCSP_Staple info already present in haproxy.sh and add commands from https://www.haproxy.com/documentation/haproxy-runtime-api/reference/set-ssl-ocsp-response/ to the update commands if ocsp is present? Or even better, integrate https://www.haproxy.com/documentation/haproxy-configuration-tutorials/ssl-tls/#enable-ocsp-stapling somehow? Thanks in advance!

@wlallemand
Copy link
Contributor Author

Hello @krezovic,

  1. The .ocsp could be used directly with "set ssl cert basecert.pem.ocsp" before committing. I'll look into this.
  2. Regarding the ocsp-update it's not possible with this current design, because the option is only working with a "crt-list" as a file, and the current config uses a directory. However we will make some changes into HAProxy so this could be done as a global option.

@krezovic
Copy link

Hi @wlallemand! Looking forward to proper support. Thanks for your reply!

@wlallemand
Copy link
Contributor Author

wlallemand commented Mar 18, 2024

@dingensundso Any chance we could have this merged? This is stuck for a while now.

@Neilpang Neilpang merged commit 0588fc6 into acmesh-official:dev Mar 18, 2024
@Neilpang
Copy link
Member

please also update the usage here:
https://github.com/acmesh-official/acme.sh/wiki/deployhooks#10-deploy-the-cert-to-haproxy

@wlallemand
Copy link
Contributor Author

Thanks, updated!

haproxy-mirror pushed a commit to haproxy/haproxy that referenced this pull request May 31, 2024
Remove the acme.sh script since it was merged in
acmesh-official/acme.sh#4581

So people don't try to download a script which is not up to date with
the current acme.sh master.
@ser
Copy link

ser commented Jul 16, 2024

Very nice job, it would be cool to have it available - maybe it's a good time for a new release?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants