]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
ADMIN: haproxy-dump-certs: implement a certificate dumper
authorWilliam Lallemand <wlallemand@irq6.net>
Sun, 28 Sep 2025 11:33:23 +0000 (13:33 +0200)
committerWilliam Lallemand <wlallemand@irq6.net>
Sun, 28 Sep 2025 11:38:48 +0000 (13:38 +0200)
haproxy-dump0-certs is a bash script that connects to your master socket
or your stat socket in order to dump certificates from haproxy memory to
the corresponding files.

admin/cli/haproxy-dump-certs [new file with mode: 0755]

diff --git a/admin/cli/haproxy-dump-certs b/admin/cli/haproxy-dump-certs
new file mode 100755 (executable)
index 0000000..592147e
--- /dev/null
@@ -0,0 +1,198 @@
+#!/bin/bash
+#
+# Dump certificates from the HAProxy stats or master socket to the filesystem
+# Experimental script
+#
+
+set -e
+
+export BASEPATH=${BASEPATH:-/etc/haproxy}/
+export SOCKET=${SOCKET:-/var/run/haproxy-master.sock}
+export DRY_RUN=0
+export DEBUG=
+export VERBOSE=
+export M="@1 "
+
+vecho() {
+
+       [ -n "$VERBOSE" ] && echo "$@"
+       return 0
+}
+
+read_certificate() {
+       name=$1
+       crt_filename=
+       key_filename=
+
+       OFS=$IFS
+       IFS=":"
+
+       while read -r key value; do
+               case "$key" in
+                       "Crt filename")
+                               crt_filename="${value# }"
+                               key_filename="${value# }"
+                       ;;
+                       "Key filename")
+                               key_filename="${value# }"
+                       ;;
+               esac
+       done < <(echo "${M}show ssl cert ${name}" | socat "${SOCKET}" -)
+       IFS=$OFS
+
+       if [ -z "$crt_filename" ] || [ -z "$key_filename" ]; then
+               echo "error: can't dump \"$name\", crt/key filename details not found in \"show ssl cert\"" >&2
+               return 1
+       fi
+
+       # handle fields without a crt-base/key-base
+       [ "${crt_filename:0:1}" != "/" ] && crt_filename="${BASEPATH}${crt_filename}"
+       [ "${key_filename:0:1}" != "/" ] && key_filename="${BASEPATH}${key_filename}"
+
+       vecho "name:$name"
+       vecho "crt:$crt_filename"
+       vecho "key:$key_filename"
+
+       export NAME="$name"
+       export CRT_FILENAME="$crt_filename"
+       export KEY_FILENAME="$key_filename"
+
+       return 0
+}
+
+dump_certificate() {
+       name=$1
+       crt_filename=$2
+       key_filename=$3
+
+       tmp="tmp.${RANDOM}"
+       d="old.$(date +%s)"
+
+       if ! touch "${crt_filename}.${tmp}" || ! touch "${key_filename}.${tmp}"; then
+               echo "error: can't dump \"$name\", can't create tmp files" >&2
+               return 1
+       fi
+
+       echo "${M}dump ssl cert ${name}" | socat "${SOCKET}" - | openssl pkey >> "${key_filename}.${tmp}"
+       # use crl2pkcs7 as a way to dump multiple x509, storeutl could be used in modern versions of openssl
+       echo "${M}dump ssl cert ${name}" | socat "${SOCKET}" - | openssl crl2pkcs7 -nocrl -certfile /dev/stdin | openssl pkcs7 -print_certs  >> "${crt_filename}.${tmp}"
+
+       if ! cmp -s <(openssl x509 -in "${crt_filename}.${tmp}" -pubkey -noout) <(openssl pkey -in "${key_filename}.${tmp}" -pubout); then
+               echo "Error: Private key \"${key_filename}.${tmp}\"  and public key \"${crt_filename}.${tmp}\" don't match" >&2
+               return 1
+       fi
+
+       # move the current certificates to ".old.timestamp"
+       mv "${crt_filename}" "${crt_filename}.${d}"
+       [ "${crt_filename}" != "${key_filename}" ] && mv "${key_filename}" "${key_filename}.${d}"
+
+       mv "${crt_filename}.${tmp}" "${crt_filename}"
+       [ "${crt_filename}" != "${key_filename}" ] && mv "${key_filename}.${tmp}" "${key_filename}"
+
+       return 0
+}
+
+dump_all_certificates() {
+       echo "${M}show ssl cert" | socat "${SOCKET}" - | grep -v '^#' | grep -v '^$' | while read -r line; do
+               export NAME
+               export CRT_FILENAME
+               export KEY_FILENAME
+
+               if read_certificate "$line"; then
+                       [ "${DRY_RUN}" = "0" ] && dump_certificate "$NAME" "$CRT_FILENAME" "$KEY_FILENAME"
+               fi
+       done
+}
+
+usage() {
+       echo "Usage:"
+       echo " $0 [options]* [cert]*"
+       echo ""
+       echo " Dump certificates from the HAProxy stats or master socket to the filesystem"
+       echo " Require socat and openssl"
+       echo " EXPERIMENTAL script, backup your files!"
+       echo " The script will move your previous files to FILE.old.unixtimestamp (ex: foo.com.pem.old.1759044998)"
+
+       echo ""
+       echo "Options:"
+       echo "  -S, --master-socket <path>   Use the master socket at <path> (default: ${SOCKET})"
+       echo "  -s, --socket <path>          Use the stats socket at <path>"
+       echo "  -p, --path <path>            Specifiy a base path for relative files (default: ${BASEPATH})"
+       echo "  -n, --dry-run                Read certificates on the socket but don't dump them"
+       echo "  -d, --debug                  Debug mode, set -x"
+       echo "  -v, --verbose                Verbose mode"
+       echo "  -h, --help                   This help"
+       echo "  --                           End of options"
+       echo ""
+       echo "Examples:"
+       echo "  $0 -v -p ${BASEPATH} -S ${SOCKET}"
+       echo "  $0 -v -p ${BASEPATH} -S ${SOCKET} bar.com.rsa.pem"
+       echo "  $0 -v -p ${BASEPATH} -S ${SOCKET} -- foo.com.ecdsa.pem bar.com.rsa.pem"
+}
+
+main() {
+       while [ -n "$1" ]; do
+               case "$1" in
+                       -S|--master-socket)
+                               SOCKET="$2"
+                               M="@1 "
+                               shift 2
+                               ;;
+                       -s|--socket)
+                               SOCKET="$2"
+                               M=
+                               shift 2
+                               ;;
+                       -p|--path)
+                               BASEPATH="$2"
+                               shift 2
+                               ;;
+                       -n|--dry-run)
+                               DRY_RUN=1
+                               shift
+                               ;;
+                       -d|--debug)
+                               DEBUG=1
+                               shift
+                               ;;
+                       -v|--verbose)
+                               VERBOSE=1
+                               shift
+                               ;;
+                       -h|--help)
+                               usage "$@"
+                               exit 0
+                               ;;
+                       --)
+                               shift
+                               break
+                               ;;
+                       -*)
+                               echo "error: Unknown option '$1'" >&2
+                               usage "$@"
+                               exit 1
+                               ;;
+                       *)
+                               break
+                               ;;
+               esac
+       done
+
+       if [ -n "$DEBUG" ]; then
+               set -x
+       fi
+
+
+       if [ -z "$1" ]; then
+               dump_all_certificates
+       else
+               # compute the certificates names at the end of the command
+               while [ -n "$1" ]; do
+                       read_certificate "$1"
+                       [ "${DRY_RUN}" = "0" ] && dump_certificate "$NAME" "$CRT_FILENAME" "$KEY_FILENAME"
+                       shift
+               done
+       fi
+}
+
+main "$@"