#!/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 " export TMP 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 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 } cmp_certkey() { prev=$1 new=$2 if [ ! -f "$prev" ]; then return 1; fi if ! cmp -s <(openssl x509 -in "$prev" -noout -fingerprint -sha256) <(openssl x509 -in "$new" -noout -fingerprint -sha256); then return 1 fi return 0 } dump_certificate() { name=$1 prev_crt=$2 prev_key=$3 r="tmp.${RANDOM}" d="old.$(date +%s)" new_crt="$TMP/$(basename "$prev_crt").${r}" new_key="$TMP/$(basename "$prev_key").${r}" if ! touch "${new_crt}" || ! touch "${new_key}"; then echo "[ALERT] ($$) : can't dump \"$name\", can't create tmp files" >&2 return 1 fi echo "${M}dump ssl cert ${name}" | socat "${SOCKET}" - | openssl pkey >> "${new_key}" # 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 >> "${new_crt}" if ! cmp -s <(openssl x509 -in "${new_crt}" -pubkey -noout) <(openssl pkey -in "${new_key}" -pubout); then echo "[ALERT] ($$) : Private key \"${new_key}\" and public key \"${new_crt}\" don't match" >&2 return 1 fi if cmp_certkey "${prev_crt}" "${new_crt}"; then echo "[NOTICE] ($$) : ${crt_filename} is already up to date" >&2 return 0 fi # move the current certificates to ".old.timestamp" if [ -f "${prev_crt}" ] && [ -f "${prev_key}" ]; then mv "${prev_crt}" "${prev_crt}.${d}" [ "${prev_crt}" != "${prev_key}" ] && mv "${prev_key}" "${prev_key}.${d}" fi # move the new certificates to old place mv "${new_crt}" "${prev_crt}" [ "${prev_crt}" != "${prev_key}" ] && mv "${new_key}" "${prev_key}" 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" else echo "[WARNING] ($$) : can't dump \"$name\", crt/key filename details not found in \"show ssl cert\"" >&2 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 "[ALERT] ($$) : Unknown option '$1'" >&2 usage "$@" exit 1 ;; *) break ;; esac done if [ -n "$DEBUG" ]; then set -x fi TMP=${TMP:-$(mktemp -d)} if [ -z "$1" ]; then dump_all_certificates else # compute the certificates names at the end of the command while [ -n "$1" ]; do if ! read_certificate "$1"; then echo "[ALERT] ($$) : can't dump \"$1\", crt/key filename details not found in \"show ssl cert\"" >&2 exit 1 fi [ "${DRY_RUN}" = "0" ] && dump_certificate "$NAME" "$CRT_FILENAME" "$KEY_FILENAME" shift done fi } trap 'rm -rf -- "$TMP"' EXIT main "$@"