From: William Lallemand Date: Sun, 28 Sep 2025 11:33:23 +0000 (+0200) Subject: ADMIN: haproxy-dump-certs: implement a certificate dumper X-Git-Tag: v3.3-dev9~25 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=3a6ea8b959d5a05063fd5836c855fdba590db37a;p=thirdparty%2Fhaproxy.git ADMIN: haproxy-dump-certs: implement a certificate dumper 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. --- diff --git a/admin/cli/haproxy-dump-certs b/admin/cli/haproxy-dump-certs new file mode 100755 index 000000000..592147e88 --- /dev/null +++ b/admin/cli/haproxy-dump-certs @@ -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 Use the master socket at (default: ${SOCKET})" + echo " -s, --socket Use the stats socket at " + echo " -p, --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 "$@"