From 7aaf4432d4068ec10dff5447d5843957f729de0a Mon Sep 17 00:00:00 2001 From: William Lallemand Date: Thu, 30 Nov 2023 11:49:54 +0100 Subject: [PATCH 1/5] haproxy: sanitize the PEM in the deploy script Sanitize the PEM of the haproxy deploy script by removing the '\n', this way it could be injected directly over the CLI. --- deploy/haproxy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index d638abb8..6c1b4a68 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -147,7 +147,7 @@ haproxy_deploy() { # Create a temporary PEM file _temppem="$(_mktemp)" _debug _temppem "${_temppem}" - cat "${_ccert}" "${_cca}" "${_ckey}" >"${_temppem}" + cat "${_ccert}" "${_cca}" "${_ckey}" | grep . >"${_temppem}" _ret="$?" # Check that we could create the temporary file From 0f7be905004fba8b6d4ec59354dd623ce8aa5c33 Mon Sep 17 00:00:00 2001 From: William Lallemand Date: Thu, 16 Mar 2023 17:24:24 +0100 Subject: [PATCH 2/5] haproxy: deploy script can update existing certificate over stats socket 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.sh | 80 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index 6c1b4a68..f6ddd7b9 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -36,6 +36,14 @@ # Note: This functionality requires HAProxy was compiled against # a version of OpenSSL that supports this. # +# export DEPLOY_HAPROXY_HOT_UPDATE="yes" +# export DEPLOY_HAPROXY_STATS_SOCKET="UNIX:/run/haproxy/admin.sock" +# +# OPTIONAL: Deploy the certificate over the HAProxy stats socket without +# needing to reload HAProxy. Default is "no". +# +# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat +# address format. ######## Public functions ##################### @@ -53,6 +61,8 @@ haproxy_deploy() { DEPLOY_HAPROXY_BUNDLE_DEFAULT="no" DEPLOY_HAPROXY_ISSUER_DEFAULT="no" DEPLOY_HAPROXY_RELOAD_DEFAULT="true" + DEPLOY_HAPROXY_HOT_UPDATE_DEFAULT="no" + DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT="UNIX:/run/haproxy/admin.sock" _debug _cdomain "${_cdomain}" _debug _ckey "${_ckey}" @@ -118,6 +128,26 @@ haproxy_deploy() { Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD_DEFAULT}" fi + # HOT_UPDATE is optional. If not provided then assume "${DEPLOY_HAPROXY_HOT_UPDATE_DEFAULT}" + _getdeployconf DEPLOY_HAPROXY_HOT_UPDATE + _debug2 DEPLOY_HAPROXY_HOT_UPDATE "${DEPLOY_HAPROXY_HOT_UPDATE}" + if [ -n "${DEPLOY_HAPROXY_HOT_UPDATE}" ]; then + Le_Deploy_haproxy_hot_update="${DEPLOY_HAPROXY_HOT_UPDATE}" + _savedomainconf Le_Deploy_haproxy_hot_update "${Le_Deploy_haproxy_hot_update}" + elif [ -z "${Le_Deploy_haproxy_hot_update}" ]; then + Le_Deploy_haproxy_hot_update="${DEPLOY_HAPROXY_HOT_UPDATE_DEFAULT}" + fi + + # STATS_SOCKET is optional. If not provided then assume "${DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT}" + _getdeployconf DEPLOY_HAPROXY_STATS_SOCKET + _debug2 DEPLOY_HAPROXY_STATS_SOCKET "${DEPLOY_HAPROXY_STATS_SOCKET}" + if [ -n "${DEPLOY_HAPROXY_STATS_SOCKET}" ]; then + Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_STATS_SOCKET}" + _savedomainconf Le_Deploy_haproxy_stats_socket "${Le_Deploy_haproxy_stats_socket}" + elif [ -z "${Le_Deploy_haproxy_stats_socket}" ]; then + Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT}" + fi + # Set the suffix depending if we are creating a bundle or not if [ "${Le_Deploy_haproxy_bundle}" = "yes" ]; then _info "Bundle creation requested" @@ -142,6 +172,7 @@ haproxy_deploy() { _issuer="${_pem}.issuer" _ocsp="${_pem}.ocsp" _reload="${Le_Deploy_haproxy_reload}" + _statssock="${Le_Deploy_haproxy_stats_socket}" _info "Deploying PEM file" # Create a temporary PEM file @@ -265,15 +296,48 @@ haproxy_deploy() { fi fi - # Reload HAProxy - _debug _reload "${_reload}" - eval "${_reload}" - _ret=$? - if [ "${_ret}" != "0" ]; then - _err "Error code ${_ret} during reload" - return ${_ret} + if [ "${Le_Deploy_haproxy_hot_update}" = "yes" ]; then + # Update certificate over HAProxy stats socket. + _info "Update the certificate over HAProxy stats socket." + if _exists socat; then + _socat_cert_cmd="echo 'show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'" + _debug _socat_cert_cmd "${_socat_cert_cmd}" + eval "${_socat_cert_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Couldn't find '${_pem}' in haproxy 'show ssl cert'" + return "${_ret}" + fi + _socat_cert_set_cmd="echo -e 'set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'" + _debug _socat_cert_set_cmd "${_socat_cert_set_cmd}" + eval "${_socat_cert_set_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Can't update '${_pem}' in haproxy" + return "${_ret}" + fi + _socat_cert_commit_cmd="echo 'commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'" + _debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}" + eval "${_socat_cert_commit_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Can't commit '${_pem}' in haproxy" + return ${_ret} + fi + else + _err "'socat' is not available, couldn't update over stats socket" + fi else - _info "Reload successful" + # Reload HAProxy + _debug _reload "${_reload}" + eval "${_reload}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Error code ${_ret} during reload" + return ${_ret} + else + _info "Reload successful" + fi fi return 0 From 98a7a01dbb85ec7e8ca2fa60497b6f7db70add98 Mon Sep 17 00:00:00 2001 From: William Lallemand Date: Thu, 6 Apr 2023 16:14:44 +0200 Subject: [PATCH 3/5] haproxy: deploy script can add a new certificate over 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 --- deploy/haproxy.sh | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index f6ddd7b9..b4c021d5 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -298,15 +298,35 @@ haproxy_deploy() { if [ "${Le_Deploy_haproxy_hot_update}" = "yes" ]; then # Update certificate over HAProxy stats socket. - _info "Update the certificate over HAProxy stats socket." if _exists socat; then + # look for the certificate on the stats socket, to chose between updating or creating one _socat_cert_cmd="echo 'show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'" _debug _socat_cert_cmd "${_socat_cert_cmd}" eval "${_socat_cert_cmd}" _ret=$? if [ "${_ret}" != "0" ]; then - _err "Couldn't find '${_pem}' in haproxy 'show ssl cert'" - return "${_ret}" + _newcert="1" + _info "Creating new certificate '${_pem}' over HAProxy stats socket." + # certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate. + _socat_crtlist_show_cmd="echo 'show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'" + _debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}" + eval "${_socat_crtlist_show_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'" + return "${_ret}" + fi + # create a new certificate + _socat_new_cmd="echo 'new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'" + _debug _socat_new_cmd "${_socat_new_cmd}" + eval "${_socat_new_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Couldn't create '${_pem}' in haproxy" + return "${_ret}" + fi + else + _info "Update existing certificate '${_pem}' over HAProxy stats socket." fi _socat_cert_set_cmd="echo -e 'set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'" _debug _socat_cert_set_cmd "${_socat_cert_set_cmd}" @@ -324,6 +344,17 @@ haproxy_deploy() { _err "Can't commit '${_pem}' in haproxy" return ${_ret} fi + if [ "${_newcert}" = "1" ]; then + # if this is a new certificate, it needs to be inserted into the crt-list` + _socat_cert_add_cmd="echo 'add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'" + _debug _socat_cert_add_cmd "${_socat_cert_add_cmd}" + eval "${_socat_cert_add_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Can't update '${_pem}' in haproxy" + return "${_ret}" + fi + fi else _err "'socat' is not available, couldn't update over stats socket" fi From 36fc3210967c839884bea8e2f90a4bdf180c89a2 Mon Sep 17 00:00:00 2001 From: William Lallemand Date: Thu, 30 Nov 2023 15:22:51 +0100 Subject: [PATCH 4/5] haproxy: use the master CLI for hot update 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. --- deploy/haproxy.sh | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index b4c021d5..ef7fe45e 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -44,6 +44,11 @@ # # Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat # address format. +# +# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock" +# +# OPTIONAL: To use the master CLI with DEPLOY_HAPROXY_HOT_UPDATE="yes" instead +# of a stats socket, use this variable. ######## Public functions ##################### @@ -54,6 +59,7 @@ haproxy_deploy() { _ccert="$3" _cca="$4" _cfullchain="$5" + _cmdpfx="" # Some defaults DEPLOY_HAPROXY_PEM_PATH_DEFAULT="/etc/haproxy" @@ -148,6 +154,16 @@ haproxy_deploy() { Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT}" fi + # MASTER_CLI is optional. No defaults are used. When the master CLI is used, + # all commands are sent with a prefix. + _getdeployconf DEPLOY_HAPROXY_MASTER_CLI + _debug2 DEPLOY_HAPROXY_MASTER_CLI "${DEPLOY_HAPROXY_MASTER_CLI}" + if [ -n "${DEPLOY_HAPROXY_MASTER_CLI}" ]; then + Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_MASTER_CLI}" + _savedomainconf Le_Deploy_haproxy_stats_socket "${Le_Deploy_haproxy_stats_socket}" + _cmdpfx="@1 " # command prefix used for master CLI only. + fi + # Set the suffix depending if we are creating a bundle or not if [ "${Le_Deploy_haproxy_bundle}" = "yes" ]; then _info "Bundle creation requested" @@ -297,18 +313,25 @@ haproxy_deploy() { fi if [ "${Le_Deploy_haproxy_hot_update}" = "yes" ]; then - # Update certificate over HAProxy stats socket. + # set the socket name for messages + if [ -n "${_cmdpfx}" ]; then + _socketname="master CLI" + else + _socketname="stats socket" + fi + + # Update certificate over HAProxy stats socket or master CLI. if _exists socat; then # look for the certificate on the stats socket, to chose between updating or creating one - _socat_cert_cmd="echo 'show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'" + _socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'" _debug _socat_cert_cmd "${_socat_cert_cmd}" eval "${_socat_cert_cmd}" _ret=$? if [ "${_ret}" != "0" ]; then _newcert="1" - _info "Creating new certificate '${_pem}' over HAProxy stats socket." + _info "Creating new certificate '${_pem}' over HAProxy ${_socketname}." # certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate. - _socat_crtlist_show_cmd="echo 'show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'" + _socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'" _debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}" eval "${_socat_crtlist_show_cmd}" _ret=$? @@ -317,7 +340,7 @@ haproxy_deploy() { return "${_ret}" fi # create a new certificate - _socat_new_cmd="echo 'new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'" + _socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'" _debug _socat_new_cmd "${_socat_new_cmd}" eval "${_socat_new_cmd}" _ret=$? @@ -326,9 +349,9 @@ haproxy_deploy() { return "${_ret}" fi else - _info "Update existing certificate '${_pem}' over HAProxy stats socket." + _info "Update existing certificate '${_pem}' over HAProxy ${_socketname}." fi - _socat_cert_set_cmd="echo -e 'set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'" + _socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'" _debug _socat_cert_set_cmd "${_socat_cert_set_cmd}" eval "${_socat_cert_set_cmd}" _ret=$? @@ -336,7 +359,7 @@ haproxy_deploy() { _err "Can't update '${_pem}' in haproxy" return "${_ret}" fi - _socat_cert_commit_cmd="echo 'commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'" + _socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'" _debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}" eval "${_socat_cert_commit_cmd}" _ret=$? @@ -346,7 +369,7 @@ haproxy_deploy() { fi if [ "${_newcert}" = "1" ]; then # if this is a new certificate, it needs to be inserted into the crt-list` - _socat_cert_add_cmd="echo 'add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'" + _socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'" _debug _socat_cert_add_cmd "${_socat_cert_add_cmd}" eval "${_socat_cert_add_cmd}" _ret=$? @@ -356,7 +379,7 @@ haproxy_deploy() { fi fi else - _err "'socat' is not available, couldn't update over stats socket" + _err "'socat' is not available, couldn't update over ${_socketname}" fi else # Reload HAProxy From e09d45c84475f3d1e223c51787fd35c9fe0bada8 Mon Sep 17 00:00:00 2001 From: William Lallemand Date: Fri, 1 Dec 2023 15:29:18 +0100 Subject: [PATCH 5/5] haproxy; don't use '*' in the filename for wildcard domain 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. --- deploy/haproxy.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index ef7fe45e..4b6ca0e1 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -102,6 +102,11 @@ haproxy_deploy() { _savedomainconf Le_Deploy_haproxy_pem_name "${Le_Deploy_haproxy_pem_name}" elif [ -z "${Le_Deploy_haproxy_pem_name}" ]; then Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}" + # We better not have '*' as the first character + if [ "${Le_Deploy_haproxy_pem_name%%"${Le_Deploy_haproxy_pem_name#?}"}" = '*' ]; then + # removes the first characters and add a _ instead + Le_Deploy_haproxy_pem_name="_${Le_Deploy_haproxy_pem_name#?}" + fi fi # BUNDLE is optional. If not provided then assume "${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"