572 lines
16 KiB
Django/Jinja
572 lines
16 KiB
Django/Jinja
#!/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:
|