#!/bin/bash # HAProxy OCSP Stapling Updater # Copyright (c) 2015 Pier Carlo Chiodi - http://www.pierky.com # # https://github.com/pierky/haproxy-ocsp-stapling-updater set -o nounset VERSION="0.4.1-pre1" PROGNAME="hapos-upd" if [ -z ${OPENSSL_BIN+x} ]; then OPENSSL_BIN="openssl" fi SOCAT_BIN="socat" CERT="" VAFILE="" HAPROXY_ADMIN_SOCKET_DEFAULT="/run/haproxy/admin.sock" HAPROXY_ADMIN_SOCKET="$HAPROXY_ADMIN_SOCKET_DEFAULT" GOOD_ONLY=0 SYSLOG_PRIO="" DEBUG=0 KEEP_TEMP=0 OCSP_URL="" OCSP_HOST="" VERIFY=1 TMP="" SKIP_UPDATE=0 PARTIAL_CHAIN="" function Quit() { if [ $KEEP_TEMP -eq 0 ]; then if [ -n "$TMP" ]; then rm -r $TMP &>/dev/null fi fi exit $1 } function LogError() { MSG="$1" if [ -z "$SYSLOG_PRIO" ]; then echo "$MSG" >&2 else logger -p "$SYSLOG_PRIO" -s -- "$PROGNAME - $MSG" fi echo "$MSG" >>$TMP/log } function Error() { if [ $1 -eq 9 ]; then MSG="Error: $2" else MSG="Error processing '$CERT': $2" fi LogError "$MSG" if [ $1 -eq 9 ]; then echo "Run $PROGNAME -h for help" >&2 fi Quit $1 } function Debug() { if [ $DEBUG -eq 1 ]; then echo "$1" fi echo "$1" >>$TMP/log } function Trap() { Debug "Aborting" Quit 9 } function Usage() { echo " HAProxy OCSP Stapling Updater - $VERSION Copyright (c) 2015 Pier Carlo Chiodi - http://www.pierky.com https://github.com/pierky/haproxy-ocsp-stapling-updater Usage: $PROGNAME [options] --cert crt_full_path This script extracts and queries the OCSP server present in a certificate to obtain its revocation status, then updates HAProxy by writing the '.issuer' and the '.ocsp' files and by sending it the 'set ssl ocsp-response' command through the local UNIX admin socket. The crt_full_path argument is the full path to the certificate bundle used in haproxy 'crt' setting. End-entity (EE) certificate plus any intermediate CA certificates must be concatenated there. An OCSP query is sent to the OCSP server given on the command line (--ocsp-url and --ocsp-host argument); if these arguments are missing, URL and Host header values are automatically extracted from the certificate. If the '.issuer' file already exists it's used to build the OCSP request, otherwise the chain is extracted from crt_full_path and used to identify the issuer. Finally, it writes the related '.issuer' and .'ocsp' files and updates haproxy, using 'socat' and the local UNIX socket (--socket argument, default $HAPROXY_ADMIN_SOCKET_DEFAULT). Exit codes: 0 OK 1 openssl certificates handling error 2 OCSP server URL not found 3 string parsing / PEM manipulation error 4 OCSP error 5 haproxy management error 9 program error (wrong arguments, missing dependencies) Options: -d, --debug : don't do anything, print debug messages only. --keep-temp : keep temporary directory after exiting (for debug purposes). -g, --good-only : do not update haproxy if OCSP response certificate status value is not 'good'. -l, --syslog priority : log errors to syslog system log module. The priority may be specified numerically or as a facility.level pair (e.g. local7.error). --ocsp-url url : OCSP server URL; use this instead of the one in the EE certificate. --ocsp-host host : OCSP server hostname to be used in the 'Host:' header; use this instead of the one extracted from the OCSP server URL. --partial-chain : Allow partial certificate chain if at least one certificate is in trusted store. Useful when validating an intermediate certificate without the root CA. -s, --socket file : haproxy admin socket. If omitted, $HAPROXY_ADMIN_SOCKET_DEFAULT is used by default. This script is distributed with only one method to update haproxy: using 'socat' with a local admin-level UNIX socket. Feel free to implement other mechanisms as needed! The right section in the code is \"UPDATE HAPROXY\", at the end of the script. -v, --VAfile file : same as the openssl ocsp -VAfile option with 'file' as argument. For more details: 'man ocsp'. If file = \"-\" then the chain extracted from the certificate's bundle (or .issuer file) is used (useful for OCSP responses that don't include the signer certificate). --noverify : Do not verify OCSP response. -S, --skip-update : Do not notify haproxy of the new OCSP response. -h, --help : this help." } trap Trap INT TERM TMP="`mktemp -d -q -t $PROGNAME.XXXXXXXXXX`" # COMMAND LINE PROCESSING # ---------------------------------- while [[ $# > 0 ]] do case "$1" in -h|--help) Usage Quit 0 ;; -d|--debug) DEBUG=1 ;; --keep-temp) KEEP_TEMP=1 ;; -g|--good-only) GOOD_ONLY=1 ;; --noverify) VERIFY=0 ;; --partial-chain) PARTIAL_CHAIN="-partial_chain" ;; -l|--syslog) if [ $# -le 1 ]; then Error 9 "mandatory value is missing for $1 argument" fi SYSLOG_PRIO="$2" shift ;; --ocsp-url) if [ $# -le 1 ]; then Error 9 "mandatory value is missing for $1 argument" fi OCSP_URL="$2" shift ;; --ocsp-host) if [ $# -le 1 ]; then Error 9 "mandatory value is missing for $1 argument" fi OCSP_HOST="$2" shift ;; -c|--cert) if [ $# -le 1 ]; then Error 9 "mandatory value is missing for $1 argument" fi CERT="$2" shift ;; -v|--VAfile) if [ $# -le 1 ]; then Error 9 "mandatory value is missing for $1 argument" fi VAFILE="$2" if [ "$VAFILE" == "-" ]; then VAFILE="$TMP/chain.pem" else if [ ! -e "$VAFILE" ]; then Error 9 "VAfile does not exists: $VAFILE" fi fi shift ;; -s|--socket) if [ $# -le 1 ]; then Error 9 "mandatory value is missing for $1 argument" fi HAPROXY_ADMIN_SOCKET="$2" shift ;; -S|--skip-update) SKIP_UPDATE=1 ;; *) Error 9 "unknown option: $1" esac shift done Debug "Temporary directory: $TMP" $OPENSSL_BIN version | grep OpenSSL &>>$TMP/log if [ $? -ne 0 ]; then Error 9 "openssl binary not found; adjust OPENSSL_BIN variable in the script" fi $SOCAT_BIN -V | grep socat &>>$TMP/log if [ $? -ne 0 ]; then Error 9 "socat binary not found; adjust SOCAT_BIN variable in the script" fi if [ -z "$CERT" ]; then Error 9 "certificate not provided (--cert argument)" fi # CURRENT RESPONSE EXPIRED? # ---------------------------------- ISNEW=1 if [ -e $CERT.ocsp ]; then ISNEW=0 Debug "An OCSP response already exists: checking its expiration." $OPENSSL_BIN ocsp -respin $CERT.ocsp -text -noverify | \ grep "Next Update:" &>>$TMP/log if [ $? -eq 0 ]; then CURR_EXP=`$OPENSSL_BIN ocsp -respin $CERT.ocsp -text -noverify | grep "Next Update:" | cut -d ':' -f 2-` CURR_EXP_EPOCH=`date --date="$CURR_EXP" +%s` if [ $? -ne 0 ]; then Error 3 "can't parse Next Update from current OCSP response" fi if [ $CURR_EXP_EPOCH -lt `date +%s` ]; then Debug "Current OCSP response expiration: $CURR_EXP - expired" LogError "current OCSP response is expired: please consider running this script more frequently" else Debug "Current OCSP response expiration: $CURR_EXP - NOT expired" fi fi fi # EXTRACT EE CERTIFICATE INFO # ---------------------------------- # extract EE certificate $OPENSSL_BIN x509 -in $CERT -outform PEM -out $TMP/ee.pem &>>$TMP/log if [ $? -ne 0 ]; then Error 1 "can't extract EE certificate from $CERT" fi # get OCSP server URL if [ -z "$OCSP_URL" ]; then OCSP_URL="`$OPENSSL_BIN x509 -in $TMP/ee.pem -ocsp_uri -noout`" if [ $? -ne 0 ]; then Error 1 "can't obtain OCSP server URL from $CERT" fi if [ -z "$OCSP_URL" ]; then Error 2 "OCSP server URL not found in the EE certificate" fi Debug "OCSP server URL found: $OCSP_URL" else Debug "Using OCSP server URL from command line: $OCSP_URL" fi # check OCSP server URL format (http:// or https://) echo "$OCSP_URL" | egrep -i "(http://|https://)" &>/dev/null if [ $? -ne 0 ]; then Error 3 "OCSP server URL not in http[s]:// format" fi # get OCSP server URL host name if [ -z "$OCSP_HOST" ]; then OCSP_HOST="`echo "$OCSP_URL" | egrep -i "(http://|https://)" | cut -d'/' -f 3`" if [ $? -ne 0 -o -z "$OCSP_HOST" ]; then Error 3 "can't extract hostname from OCSP server URL $OCSP_URL" fi Debug "OCSP server hostname: $OCSP_HOST" else Debug "Using OCSP server hostname from command line: $OCSP_HOST" fi # EXTRACT CHAIN INFO # ---------------------------------- if [ -e $CERT.issuer ]; then Debug "Using existing chain ($CERT.issuer)" # copy .issuer file to temporary chain.pem cp $CERT.issuer $TMP/chain.pem &>>$TMP/log if [ $? -ne 0 ]; then Error 3 "can't copy current chain from $CERT.issuer" fi else Debug "Extracting chain from certificates bundle" # get EE certificate's fingerprint FP_EE="`$OPENSSL_BIN x509 -fingerprint -noout -in $TMP/ee.pem`" if [ $? -ne 0 -o -z "$FP_EE" ]; then Error 1 "can't obtain EE certificate's fingerprint" fi Debug "EE certificate's fingerprint: $FP_EE" # get BEGIN CERTIFICATE and END CERTIFICATE separators PEM_BEGIN_CERT="`head $TMP/ee.pem -n 1`" PEM_END_CERT="`tail $TMP/ee.pem -n 1`" # get number of certificates in the bundle file NUM_OF_CERTS=`cat $CERT | grep -e "$PEM_BEGIN_CERT" | wc -l` if [ $NUM_OF_CERTS -le 1 ]; then Error 3 "can't obtain the number of certificates in the chain" fi Debug "$NUM_OF_CERTS certificates found in the bundle" # save each certificate in the bundle into $TMP/chain-X.pem cat $CERT | \ sed -n -e "/$PEM_BEGIN_CERT/,/$PEM_END_CERT/p" | \ awk "/$PEM_BEGIN_CERT/{x=\"$TMP/chain-\" ++i \".pem\";}{print > x;}" &>>$TMP/log if [ $? -ne 0 ]; then Error 3 "can't extract certificates from bundle" fi # for each certificate that is extracted from the bundle check if # it's the EE certificate, otherwise uses it to build the chain file for c in `seq 1 $NUM_OF_CERTS`; do # check fingerprint of current and EE certificates FP="`$OPENSSL_BIN x509 -fingerprint -noout -in $TMP/chain-$c.pem`" if [ $? -ne 0 -o -z "$FP" ]; then Error 1 "can't obtain the fingerprint of the certificate n. $c in the bundle" else if [ ! "$FP" == "$FP_EE" ]; then Debug "Bundle certificate n. $c fingerprint: $FP - it's part of the chain" # current certificate is not the same as the EE; append to the chain cat $TMP/chain-$c.pem >> $TMP/chain.pem else Debug "Bundle certificate n. $c fingerprint: $FP - EE certificate" fi fi done fi # check if the EE certificate validates against the chain $OPENSSL_BIN verify $PARTIAL_CHAIN -CAfile $TMP/chain.pem $TMP/ee.pem &>>$TMP/log if [ $? -ne 0 ]; then if [ -e $CERT.issuer ]; then Error 1 "can't validate the EE certificate against the existing chain; if it has been changed recently consider removing the current $CERT.issuer file and let this script to figure out a new one" else Error 1 "can't validate the EE certificate against the extracted chain" fi fi # OCSP # ---------------------------------- # query the OCSP server and save its response $OPENSSL_BIN version | grep "OpenSSL 1.0" &>/dev/null if [ $? -eq 0 ]; then # OpenSSL 1.0.x $OPENSSL_BIN ocsp $PARTIAL_CHAIN -issuer $TMP/chain.pem -cert $TMP/ee.pem \ -respout $TMP/ocsp.der -noverify \ -no_nonce -url $OCSP_URL -header "Host" "$OCSP_HOST" &>>$TMP/log else $OPENSSL_BIN ocsp $PARTIAL_CHAIN -issuer $TMP/chain.pem -cert $TMP/ee.pem \ -respout $TMP/ocsp.der -noverify \ -no_nonce -url $OCSP_URL -header "Host=$OCSP_HOST" &>>$TMP/log fi if [ $? -ne 0 ]; then Error 1 "can't receive the OCSP server response" fi # process the OCSP response VERIFYOPT="" if [ $VERIFY -eq 0 ]; then VERIFYOPT="-noverify" fi if [ -z "$VAFILE" ]; then $OPENSSL_BIN ocsp $PARTIAL_CHAIN $VERIFYOPT -issuer $TMP/chain.pem -cert $TMP/ee.pem \ -respin $TMP/ocsp.der -no_nonce -CAfile $TMP/chain.pem \ -out $TMP/ocsp.txt &>>$TMP/ocsp-verify.txt else $OPENSSL_BIN ocsp $PARTIAL_CHAIN $VERIFYOPT -issuer $TMP/chain.pem -cert $TMP/ee.pem \ -respin $TMP/ocsp.der -no_nonce -CAfile $TMP/chain.pem \ -VAfile $VAFILE \ -out $TMP/ocsp.txt &>>$TMP/ocsp-verify.txt fi if [ $? -ne 0 ]; then Error 1 "can't receive OCSP response" fi if [ $VERIFY -eq 1 ]; then Debug "OCSP response verification results: `cat $TMP/ocsp-verify.txt`" cat $TMP/ocsp-verify.txt | grep "Response verify OK" &>>$TMP/log if [ $? -ne 0 ]; then grep "signer certificate not found" $TMP/ocsp-verify.txt &>/dev/null if [ $? -eq 0 ]; then Error 4 "OCSP response verification failure: signer certificate not found; try with '--VAfile -' or '--VAfile OCSP-response-signing-certificate-file' arguments" else Error 4 "OCSP response verification failure." fi fi fi Debug "OCSP response: `cat $TMP/ocsp.txt`" if [ $GOOD_ONLY -eq 1 ]; then cat $TMP/ocsp.txt | head -n 1 | grep ": good" &>>$TMP/log if [ $? -ne 0 ]; then Error 4 "OCSP response, certificate status not good" fi fi # UPDATE HAPROXY # ---------------------------------- # Status: # - $TMP/ocsp.der contains the OCSP response, DER format # - $TMP/ocsp.txt contains the textual OCSP response as produced # by openssl # - the OCSP response has been verified against the chain or # the --VAfile if [ $DEBUG -eq 0 ]; then # update .ocsp and .issuer files cp $TMP/ocsp.der $CERT.ocsp &>>$TMP/log if [ $? -ne 0 ]; then Error 5 "can't update $CERT.ocsp file" fi if [ ! -e $CERT.issuer ]; then cp $TMP/chain.pem $CERT.issuer &>>$TMP/log if [ $? -ne 0 ]; then Error 5 "can't update $CERT.issuer file" fi fi if [ $SKIP_UPDATE -eq 0 ]; then if [ $ISNEW -eq 1 ]; then # no .ocsp file found, maybe it's an initial run Debug "Reloading haproxy." service haproxy reload if [ $? -ne 0 ]; then Error 5 "can't reload haproxy with 'service haproxy reload'" fi else # update haproxy via local UNIX socket Debug "Updating haproxy." echo "set ssl ocsp-response `base64 -w 0 $TMP/ocsp.der`" | $SOCAT_BIN stdio $HAPROXY_ADMIN_SOCKET &>>$TMP/log if [ $? -ne 0 ]; then Error 5 "can't update haproxy ssl ocsp-response using $HAPROXY_ADMIN_SOCKET socket" fi fi else Debug "Not notifying haproxy because skip-update is set." fi else Debug "Debug mode: haproxy update skipped." fi # remove temporary files and quit with success Quit 0 # vim: set tabstop=4 shiftwidth=4 expandtab: