diff --git a/README.md b/README.md index 0158237..a5efadd 100755 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Script to automate management of letsencrypt SSL certificates, supporting wildca mode is one of: deploy|renew|generate|test + -A In generate/renew modes, Also deploy certs if needed -c Cron mode - only output if something is done. -f Push out certs even if they haven't changed. -i Iniitalise new config file in /Users/rpearce/.lehook/config diff --git a/lehook.sh b/lehook.sh index bb83657..73fea0a 100755 --- a/lehook.sh +++ b/lehook.sh @@ -3,17 +3,6 @@ DIR=$HOME/.lehook configfile=$DIR/config VALIDMODES="deploy|renew|generate|test" -function doinstall() { - echo "Creating hardlinks in $DIR..." - cp -f $0 $DIR/lehook.sh - for x in pre post deploy; do - ln -f $DIR/lehook.sh $DIR/lehook-${x}.sh - done - ln -f $DIR/lehook.sh /usr/local/bin/lehook.sh - echo "Install complete. Files are in $DIR." - echo "Main binary in /usr/local/bin/lehook.sh." -} - function doinit() { mkdir -p $DIR if [[ ! -e $configfile ]]; then @@ -67,6 +56,20 @@ function doinstall() { echo "Main binary in /usr/local/bin/lehook.sh." } +function log() { + local showtoo txt + if [[ $1 == "-v" ]]; then + showtoo=1 + shift 1 + else + showtoo=0 + fi + txt="$(date): $*" + + echo "$txt" >>$LOG + [[ $showtoo -eq 1 ]] && echo "$txt" +} + function out() { local nl nl="\n" @@ -99,7 +102,7 @@ function copycert() { dec } function dossh() { - local h desc cmd rv + local h desc cmd rv err h=$1 desc="$2" cmd="$3" @@ -112,7 +115,7 @@ function dossh() { if [[ $? -eq 0 ]]; then [[ $quiet -ne 1 ]] && echo "Ok" else - [[ $quiet -ne 1 ]] && echo "$err" + [[ $quiet -ne 1 ]] && echo "Error: [$err]" rv=1 fi dec @@ -125,9 +128,11 @@ function usage() { echo echo " mode is one of: $VALIDMODES" echo + echo " -A In generate/renew modes, Also deploy certs if needed" echo " -c Cron mode - only output if something is done." echo " -f Push out certs even if they haven't changed." - echo " -i Install files in $DIR and generate config if it doesn't exist." + echo " -i Iniitalise new config file in $configfile" + echo " -I Install binary symlinks in home dir" echo " -r remotes Only push certs to the given remotes [default: $remotes]" echo " -s services Only restart the given services [default: $services]" echo " -d domain Only push cert for given domain [default: $domains]." @@ -171,17 +176,20 @@ quiet=0 verbose=0 cronmode=0 init=0 +alsodeploy=0 recurseargs="" if [[ -e $configfile ]]; then . $configfile fi -ARGS="cd:fhiIqr:s:v" -# TODO: add renew mode -# TODO: add generate mode +ARGS="Acd:fhiIqr:s:v" while getopts "$ARGS" i; do case "$i" in + A) + alsodeploy=1 + ;; + c) cronmode=1 recurseargs="$recurseargs -c" @@ -235,12 +243,20 @@ if [[ ! -e $configfile ]]; then exit 1 fi +LOG=/var/log/lehook.log + if [[ $0 =~ pre|post|deploy ]]; then + log -v "lehook running with cmd="$0" args='$*' CERTBOT_DOMAIN='$CERTBOT_DOMAIN' CERTBOT_VALIDATION='$CERTBOT_VALIDATION'" set -e set -u set -o pipefail if [[ -z $CERTBOT_DOMAIN ]]; then - echo "Error: no domain env var received from certbot. vars are:" + log -v "Error: no \$CERTBOT_DOMAIN env var received from certbot. vars are:" + set | grep CERTBOT + exit 1 + fi + if [[ -z $CERTBOT_VALIDATION ]]; then + log -v "Error: no \$CERTBOT_VALIDATION env var received from certbot. vars are:" set | grep CERTBOT exit 1 fi @@ -265,112 +281,12 @@ else fi fi -function needsrestart() { - local sname rv - sname=$1 - rv=1 - if [[ $sname == "dovecot" ]]; then - rv=0 - fi - return $rv -} - -function wait_for_dns_update() { # wait_for_dns_update domain value - local ns record - record="_acme-challenge.$1" - value="$2" - for ns in $check_nameservers; do - [[ $quiet -eq 0 || $cronmode -eq 1 ]] && echo -n "Waiting for challenge record to update on $ns..." - res=$(dig +short @${ns} $record IN TXT | tr -d '"' 2>&1) - while [[ $res != $value ]]; do - sleep 5 - echo -n "." - res=$(dig +short @${ns} $record IN TXT | tr -d '"' 2>&1) - done - echo "done" - done -} - -if [[ $0 == *pre* ]]; then - DOMAIN="$CERTBOT_DOMAIN" - DATA="$CERTBOT_VALIDATION" - cmd=$(printf "server %s\nlocal $LOCALADDR\n update add _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" "${DNSSERVER}" "${DOMAIN}" "${TTL}" "${DATA}") - #echo "PREHOOK - ABOUT TO RUN:" - #echo "$cmd" | sed -e 's/^/ /' - echo "$cmd" | $NSUPDATE - rv=$? - wait_for_dns_update "$DOMAIN" "$DATA" - exit $rv -elif [[ $0 == *post* ]]; then - DOMAIN="$CERTBOT_DOMAIN" - DATA="$CERTBOT_VALIDATION" - cmd=$(printf "server %s\nlocal $LOCALADDR\n update delete _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" "${DNSSERVER}" "${DOMAIN}" "${TTL}" "${DATA}") - #echo "POSTHOOK - ABOUT TO RUN" - #echo "$cmd" | sed -e 's/^/ /' - echo "$cmd" | $NSUPDATE - rv=$? - wait_for_dns_update "$DOMAIN" "" - exit $rv -elif [[ $0 == *deploy* ]]; then - DOMAIN="$CERTBOT_DOMAIN" - /usr/local/bin/lehook.sh deploy -d $DOMAIN $recurseargs - exit 1 -fi - - - -rv=0 -if [[ $mode == "generate" ]]; then - rv=0 - for this in $domains; do - res=$(certbot certonly -n --manual --preferred-challenges=dns --email $email --agree-tos --manual-auth-hook $DIR/lehook-pre.sh --manual-cleanup-hook $DIR/lehook-post.sh --deploy-hook $DIR/lehook-deploy.sh -d *.${this} 2>&1) - rv=$((rv + $?)) - [[ $verbose -eq 1 ]] && echo "$res" - done -elif [[ $mode == "renew" ]]; then - rv=0 - extraargs="" - [[ $force -eq 1 ]] && extraargs="$extraargs --force-renewal" - for this in $domains; do - res=$(certbot renew -n --manual --preferred-challenges=dns --email $email --agree-tos --manual-auth-hook $DIR/lehook-pre.sh --manual-cleanup-hook $DIR/lehook-post.sh --deploy-hook $DIR/lehook-deploy.sh $extraargs --cert-name ${this} 2>&1) - rv=$((rv + $?)) - [[ $verbose -eq 1 ]] && echo "$res" - if [[ "$res" =~ "not due for" ]]; then - if [[ $quiet -eq 0 && $cronmode -eq 0 ]]; then - exp=$(certbot certificates --cert-name $this 2>&1 | grep Expiry | sed -e 's/^.*Date: //') - echo "$this not due for renewal yet. Expiry: $exp" - fi - elif [[ "$res" =~ "renewals succeeded" ]]; then - if [[ $quiet -eq 0 || $cronmode -eq 1 ]]; then - exp=$(certbot certificates --cert-name $this 2>&1 | grep Expiry | sed -e 's/^.*Date: //') - echo "$this has been renewed. New expiry: $exp" - fi - else - echo "Error renewing $this. Certbot output:" - echo "$res" - fi - done -elif [[ $mode == "test" ]]; then - nowdate="$(date +%s)" - echo "== Test mode." - echo "== About to run pre-hook" - export CERTBOT_DOMAIN="${domains%% *}" # just use the first one - export CERTBOT_VALIDATION="test-${nowdate}" - $DIR/lehook-pre.sh - echo "== Pre-hook done. Checking DNS record:" - dig -b $LOCALADDR @${DNSSERVER} _acme-challenge.${domains%% *}. IN txt - echo -n "The above should show a TXT record saying '$nowdate'.... hit ENTER: " - read dummy - echo "== About to run post-hook" - export CERTBOT_DOMAIN="${domains%% *}" - export CERTBOT_VALIDATION="test-${nowdate}" - echo "== Post-hook done. Checking DNS record:" - $DIR/lehook-post.sh - dig -b $LOCALADDR @${DNSSERVER} _acme-challenge.${domains%% *}. IN txt - echo -n "The above should NOT show a TXT record any more. Hit ENTER: " - read dummy - exit 1 -elif [[ $mode == "deploy" ]]; then +function dodeploy() { + local certdirs d rv + local r newcerta ip domain dest_cert dest_priv + local local_cert_sum local_priv_sum needupate + local isrh checkcmd reloadcmd enabled + local nok nfail okservs failservs s certdirs="" for d in $domains; do thisone=$certbase/$d @@ -427,15 +343,16 @@ elif [[ $mode == "deploy" ]]; then out "* Restarting services" inc - ssh root@$r "grep -qi 'Release 6' /etc/redhat-release 2>/dev/null" - if [[ $? -eq 0 ]]; then - checkcmd="chkconfig" + isrh=$(ssh root@$r "test -f /etc/redhat-release && echo yes || echo no") + if [[ $isrh == "no" ]]; then + checkcmd="service --status-all" reloadcmd="service XX restart && echo Ok || echo failed" else checkcmd="systemctl list-unit-files --state=enabled" reloadcmd="systemctl restart XX" fi - enabled=$(ssh root@$r "$checkcmd" | awk '/:on|nabled/ { print $1 }') + + enabled=$(ssh root@$r "$checkcmd 2>&1" | awk '/:on|nabled/ { print $1 } ($2 == "+") { print $4 }') nok=0 nfail=0 okservs="" @@ -443,9 +360,6 @@ elif [[ $mode == "deploy" ]]; then for s in $services; do if [[ $enabled == *$s* ]]; then reloadcmd=$(echo "$reloadcmd" | sed -e "s/XX/$s/") - if needsrestart $s; then - reloadcmd=$(echo "$reloadcmd" | sed -e "s/reload/restart/") - fi dossh $r $s "$reloadcmd" thisrv=$? rv=$((rv + $thisrv)) @@ -481,5 +395,149 @@ elif [[ $mode == "deploy" ]]; then fi dec done +} + +function wait_for_dns_update() { # wait_for_dns_update [-v for inverse match] domain value + local ns record secs grv wantgrv verb + if [[ $1 == "-v" ]]; then + # want grep NOT to match + verb="does NOT contain" + wantgrv=1 + shift 1 + else + # want grep to match + verb="contains" + wantgrv=0 + fi + record="_acme-challenge.$1" + value="$2" + secs=5 + + log -v "Delaying until dns is done with:" + log -v " servers: $check_nameservers" + log -v " record: $record" + log -v "desired value: $verb $value" + log -v " delay: $secs" + for ns in $check_nameservers; do + [[ $verbose -eq 1 || $quiet -eq 0 || $cronmode -eq 1 ]] && echo -n "Waiting for challenge record to update on $ns..." + res=$(dig +short @${ns} $record IN TXT | tr -d '"' 2>&1) + (echo "$res" | grep -q "$value") && grv=0 || grv=1 + while [[ $grv -ne $wantgrv ]]; do + sleep $secs + [[ $verbose -eq 1 || $quiet -eq 0 || $cronmode -eq 1 ]] && echo -n "." + res=$(dig +short @${ns} $record IN TXT | tr -d '"' 2>&1) + (echo "$res" | grep -q "$value") && grv=0 || grv=1 + done + [[ $verbose -eq 1 || $quiet -eq 0 || $cronmode -eq 1 ]] && echo "done" + done + log -v "All DNS updates done." + return 0 +} + +if [[ $0 == *pre* ]]; then + DOMAIN="$CERTBOT_DOMAIN" + DATA="$CERTBOT_VALIDATION" + cmd=$(printf "server %s\nlocal $LOCALADDR\n update add _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" "${DNSSERVER}" "${DOMAIN}" "${TTL}" "${DATA}") + log -v "PREHOOK - Adding challenge key '$DATA' for domain "$DOMAIN" via:" + log -v "$cmd" | sed -e 's/^/ /' + echo "$cmd" | $NSUPDATE + rv=$? + log -v "PREHOOK - Done, now waiting for dns update." + wait_for_dns_update "$DOMAIN" "$DATA" + log -v "PREHOOK - Waiting for any children" + wait + log -v "PREHOOK - About to exit with code $rv" + exit $rv +elif [[ $0 == *post* ]]; then + DOMAIN="$CERTBOT_DOMAIN" + DATA="$CERTBOT_VALIDATION" + cmd=$(printf "server %s\nlocal $LOCALADDR\n update delete _acme-challenge.%s. %d in TXT \"%s\"\nsend\n" "${DNSSERVER}" "${DOMAIN}" "${TTL}" "${DATA}") + #echo "POSTHOOK - ABOUT TO RUN" + #echo "$cmd" | sed -e 's/^/ /' + log -v "POSTHOOK - Removing challenge key '$DATA' via:" + log -v "$cmd" | sed -e 's/^/ /' + echo "$cmd" | $NSUPDATE + rv=$? + log -v "POSTHOOK - Done, now waiting for dns update." + wait_for_dns_update -v "$DOMAIN" "$DATA" + log -v "POSTHOOK - Waiting for any children" + wait + log -v "POSTHOOK - About to exit with code $rv" + exit $rv +elif [[ $0 == *deploy* ]]; then + DOMAIN="$CERTBOT_DOMAIN" + /usr/local/bin/lehook.sh deploy -d $DOMAIN $recurseargs + exit 1 +fi + + + +rv=0 +if [[ $mode == "generate" ]]; then + rv=0 + for this in $domains; do + res=$(certbot certonly -n --manual --preferred-challenges=dns --email $email --agree-tos --manual-auth-hook $DIR/lehook-pre.sh --manual-cleanup-hook $DIR/lehook-post.sh --deploy-hook $DIR/lehook-deploy.sh --expand -d "${this},*.${this}" 2>&1) + rv=$((rv + $?)) + [[ $verbose -eq 1 ]] && echo "$res" + done + if [[ $rv -eq 0 && $alsodeploy -eq 1 ]]; then + dodeploy + rv=$? + fi +elif [[ $mode == "renew" ]]; then + rv=0 + extraargs="" + renewed=0 + [[ $force -eq 1 ]] && extraargs="$extraargs --force-renewal" + cp -f /dev/null /tmp/lh-renew + for this in $domains; do + [[ $verbose -eq 1 ]] && echo "Will run: certbot renew -n --manual --preferred-challenges=dns --email $email --agree-tos --manual-auth-hook $DIR/lehook-pre.sh --manual-cleanup-hook $DIR/lehook-post.sh --deploy-hook $DIR/lehook-deploy.sh $extraargs --cert-name ${this} 2>&1" | tee -a /tmp/lh-renew + res=$(certbot renew -n --manual --preferred-challenges=dns --email $email --agree-tos --manual-auth-hook $DIR/lehook-pre.sh --manual-cleanup-hook $DIR/lehook-post.sh --deploy-hook $DIR/lehook-deploy.sh $extraargs --cert-name ${this} 2>&1 | tee -a /tmp/lh-renew) + rv=$((rv + $?)) + [[ $verbose -eq 1 ]] && echo "$res" + if [[ "$res" =~ "not due for" ]]; then + if [[ $quiet -eq 0 && $cronmode -eq 0 ]]; then + exp=$(certbot certificates --cert-name $this 2>&1 | grep Expiry | sed -e 's/^.*Date: //') + echo "$this not due for renewal yet. Expiry: $exp" + fi + elif [[ "$res" =~ "renewals succeeded" ]]; then + renewed=$((renewed + 1)) + if [[ $quiet -eq 0 || $cronmode -eq 1 ]]; then + exp=$(certbot certificates --cert-name $this 2>&1 | grep Expiry | sed -e 's/^.*Date: //') + echo "$this has been renewed. New expiry: $exp" + fi + else + echo "Error renewing $this. Certbot output:" + echo "$res" + fi + done + if [[ $rv -eq 0 && $alsodeploy -eq 1 && $renewed -gt 0 ]]; then + dodeploy + rv=$? + fi +elif [[ $mode == "test" ]]; then + nowdate="$(date +%s)" + echo "== Test mode." + echo "== About to run pre-hook" + export CERTBOT_DOMAIN="${domains%% *}" # just use the first one + export CERTBOT_VALIDATION="test-${nowdate}" + $DIR/lehook-pre.sh + echo "== Pre-hook done. Checking DNS record:" + dig -b $LOCALADDR @${DNSSERVER} _acme-challenge.${domains%% *}. IN txt + echo -n "The above should show a TXT record saying '$nowdate'.... hit ENTER: " + read dummy + echo "== About to run post-hook" + export CERTBOT_DOMAIN="${domains%% *}" + export CERTBOT_VALIDATION="test-${nowdate}" + echo "== Post-hook done. Checking DNS record:" + $DIR/lehook-post.sh + dig -b $LOCALADDR @${DNSSERVER} _acme-challenge.${domains%% *}. IN txt + echo -n "The above should NOT show a TXT record any more. Hit ENTER: " + read dummy + exit 1 +elif [[ $mode == "deploy" ]]; then + dodeploy + rv=$? fi exit $rv +