diff --git a/haproxy/tasks/haproxy-ssl.yml b/haproxy/tasks/haproxy-ssl.yml new file mode 100644 index 0000000..7750336 --- /dev/null +++ b/haproxy/tasks/haproxy-ssl.yml @@ -0,0 +1,17 @@ +--- +- block: + - name: Install the socat binary needed to talk to the haproxy socket + apt: name=socat state=latest update_cache=yes cache_valid_time=3600 + + - name: Install a script that refreshes the OCSP configuration and reloads haproxy if needed + template: src=hapos-upd.j2 dest=/usr/local/bin/hapos-upd owner=root group=root mode=0755 + + - name: Install a cron job that refreshes the OCSP configuration + cron: + name: "Refresh haproxy OCSP information" + user: root + special_time: daily + job: "/usr/local/bin/hapos-upd --cert {{ haproxy_cert_dir }}/haproxy.pem -v {{ letsencrypt_acme_certs_dir }}/fullchain -s {{ haproxy_admin_socket }} >/var/log/hapos-upd.log 2>&1" + + tags: [ 'haproxy', 'letsencrypt', 'ssl', 'ssl_ocsp' ] + diff --git a/haproxy/tasks/main.yml b/haproxy/tasks/main.yml index 415a86d..d5e3fce 100644 --- a/haproxy/tasks/main.yml +++ b/haproxy/tasks/main.yml @@ -8,6 +8,9 @@ when: - haproxy_letsencrypt_managed - letsencrypt_acme_install is defined +- include: haproxy-ssl.yml + when: + - haproxy_letsencrypt_managed - include: haproxy-nagios.yml when: diff --git a/haproxy/templates/hapos-upd.j2 b/haproxy/templates/hapos-upd.j2 new file mode 100644 index 0000000..4b399bd --- /dev/null +++ b/haproxy/templates/hapos-upd.j2 @@ -0,0 +1,571 @@ +#!/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: