diff options
-rwxr-xr-x | lets-ca.sh | 374 | ||||
-rw-r--r-- | lets-ca.sh-cron | 156 |
2 files changed, 530 insertions, 0 deletions
diff --git a/lets-ca.sh b/lets-ca.sh new file mode 100755 index 0000000..30ba356 --- /dev/null +++ b/lets-ca.sh @@ -0,0 +1,374 @@ +#!/bin/bash +set -euo pipefail +IFS=$'\n\t' +############# + +# This script creates a key and csr, and uses acme-tiny to have the csr signed +# with lets encrypt. + +# by Dennis Eriksen, dennis@eriksen.im, 2015-12-16 + +############# + + + + +################################################################################ + +# TODO + +################################################################################ + +# Handling of getopts. If you trigger -b after -r, -b doesn't count. Also, you +# don't want to be able to trigger -s and -r, etc. + +# There should probably be a file indicating which domains each cert is valid +# for.. Like $domain/SAN, with an entry for each domain. + + +################################################################################ + +# Config + +################################################################################ + +readonly CERTDIR="/etc/letsencrypt/certs" +readonly CERTDEST="/etc/ssl/letsencrypt" +readonly OPENSSLCONF="/etc/letsencrypt/openssl.cnf" +readonly ACCOUNTKEY="/etc/letsencrypt/account.key" +readonly -a ARGS=("$@") + +readonly LETSCAUSER="letsencrypt" +readonly LETSCAGROUP="letsencrypt" + +readonly CERTKEYOWNER="root" +readonly CERTGROUP="ssl-cert" + + + +readonly INTERMEDIATE="/etc/ssl/lets-encrypt-x1-cross-signed.pem" + + +readonly ACMETINY="/usr/local/sbin/acme_tiny.py" + +readonly CHALLENGEDIR="/var/www/letsencrypt-challenges" + +readonly NGINXCONFSUFFIX="" + +BATCHMODE=FALSE + +QUIET=FALSE + +################################################################################ + +# Usage + +################################################################################ + +usage() { + echo "Yes. This is Usage. Hello." +} + + + +################################################################################ + +# say + +################################################################################ + +echo() { + if [[ ! "$QUIET" == TRUE ]]; then + builtin echo $1 + fi +} + + + +################################################################################ + +# cmdline + +################################################################################ + +cmdline() { + + local bflag=FALSE + local dflag=FALSE + local qflag=FALSE + local rflag=FALSE + local sflag=FALSE + + while getopts "xqhbr:s:d:" OPTION + do + case ${OPTION} in + b) + bflag=TRUE + BATCHMODE=TRUE + ;; + h) + usage + exit 0 + ;; + x) + readonly DEBUG='-x' + echo "-x was triggered" + echo "\${ARGS{[@]} = ${ARGS[@]}" + set -x + ;; + s) + sflag=TRUE + sign ${OPTARG} + ;; + d) + dflag=TRUE + deploy ${OPTARG} + ;; + q) + QUIET=TRUE + ;; + r) + rflag=TRUE + req ${OPTARG} + ;; + \?) + usage && exit 1 + ;; + :) + usage && exit 1 + ;; + esac + done + shift $((OPTIND-1)) + + if [[ "$bflag" == TRUE ]] && [[ ! "$rflag" == TRUE ]]; then + echo "-b is only used with -r." + echo "" + usage && exit 1 + fi + + return 0 +} + + +################################################################################ + +# deploy + +################################################################################ + +deploy() { + local domain="" + + # Domains are provided separated by spaces. + IFS=' ' read -r -a domain <<< $1 + + if [[ ! ${#domain[@]} == 1 ]]; then + echo "-d only takes one domain." + exit 1 + elif [[ ! -d "$CERTDIR/${domain[0]}" ]]; then + echo "\"${domain[0]}\" does not exist in $CERTDIR" + exit 1 + elif [[ ! -f "$CERTDIR/${domain[0]}/${domain[0]}.key" ]]; then + echo "${domain[0]}.key seems to be missing." + exit 1 + elif [[ ! -f "$CERTDIR/${domain[0]}/${domain[0]}.crt" ]]; then + echo "${domain[0]}.crt seems to be missing." + echo "Are you sure you've signed the CSR?" + exit 1 + fi + + if [[ ! -d "$CERTDEST" ]]; then + mkdir -p $CERTDEST + chgrp $CERTGROUP $CERTDEST + fi + + + # Lets's copy! + echo "Copying ${domain[0]}.crt and ${domain[0]}.key to $CERTDEST" + cp --preserve=all $CERTDIR/${domain[0]}/${domain[0]}.{crt,key} $CERTDEST + + # If the certificate is meant for nginx, we want to combine the cert and the + # intermediary certificate. + if [[ -f "/etc/nginx/sites-enabled/${domain[0]}$NGINXCONFSUFFIX" ]] || \ + [[ -f "$CERTDEST/${domain[0]}.pem" ]]; then + echo "Detected nginx-config for ${domain[0]}, or existing pem-file." + echo "Moving crt to pem, and appending intermediate certificate." + mv $CERTDEST/${domain[0]}.crt $CERTDEST/${domain[0]}.pem + cat $INTERMEDIATE >> $CERTDEST/${domain[0]}.pem + fi + + echo "" + echo "Deployment finished. You should probably reload your configuration." + + + exit 0 + + +} + + +################################################################################ + +# req + +################################################################################ + +req() { + local domain="" + local i=0 + local batch="" + + # Domains are provided separated by spaces. + IFS=' ' read -r -a domain <<< $1 + + # Is batchmode triggered? + [[ "$BATCHMODE" == TRUE ]] && batch="-batch" + + if [[ -z "$CERTDIR" ]] || [[ -z "${domain[0]}" ]]; then + echo "\$CERTDIR and/or \${domain[0]} is empty. Something is wrong." + exit 1 + fi + + if [[ ! -d $CERTDIR ]]; then + mkdir -p $CERTDIR + chmod 750 $CERTDIR + chown $LETSCAUSER:$CERTGROUP $CERTDIR + fi + + if [[ -d "$CERTDIR/${domain[0]}" ]]; then + echo "The directory $CERTDIR/${domain[0]} already exists." + echo "If you continue, it will be deleted." + echo "" + read -p "Press Y to continue, or any other button to abort." -n 1 -r + + if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then + echo "Okay then. Live long and prosper." + exit 1 + else + echo "" + rm -r "$CERTDIR/${domain[0]}" && mkdir -p "$CERTDIR/${domain[0]}" + fi + + else + mkdir -p "$CERTDIR/${domain[0]}" + fi + + # if there is only one domain + if [[ ${#domain[@]} == 1 ]]; then + echo "Only one domain - ${domain[0]}" + openssl req -new -sha256 -nodes -out $CERTDIR/${domain[0]}/${domain[0]}.csr\ + -keyout $CERTDIR/${domain[0]}/${domain[0]}.key -config <(cat $OPENSSLCONF\ + | sed -r "s/REPLACE/${domain[0]}/") $batch + + # if there is more than one domain + else + echo "Several domains: " + + SANtext="[SAN]\nsubjectAltName=" + + for SAN in "${domain[@]}"; do + + echo "$SAN" + + if [[ ! "$SAN" == "${domain[0]}" ]]; then + ((i+=1)) && [[ $i > 1 ]] && SANtext+="," + SANtext+="DNS:$SAN" + fi + done + echo "Common Name will be ${domain[0]}" + echo $SANtext + + openssl req -new -sha256 -nodes -out $CERTDIR/${domain[0]}/${domain[0]}.csr\ + -keyout $CERTDIR/${domain[0]}/${domain[0]}.key -reqexts SAN -config <(cat\ + $OPENSSLCONF <(printf "$SANtext") | sed -r "s/REPLACE/${domain[0]}/") \ + $batch + + fi + + chown -R $LETSCAUSER:$CERTGROUP $CERTDIR/${domain[0]} + chmod 750 $CERTDIR/${domain[0]} + + chown $CERTKEYOWNER:$CERTGROUP $CERTDIR/${domain[0]}/${domain[0]}.key + chmod 440 $CERTDIR/${domain[0]}/${domain[0]}.{csr,key} + + exit 0 + +} + + + +################################################################################ + +# sign + +################################################################################ + +sign() { + local domain="" + + # Domains are provided separated by spaces. + IFS=' ' read -r -a domain <<< $1 + + if [[ ! ${#domain[@]} == 1 ]]; then + echo "-s only takes one domain." + exit 1 + elif [[ ! -d "$CERTDIR/${domain[0]}" ]]; then + echo "\"${domain[0]}\" does not exist in $CERTDIR" + exit 1 + elif [[ ! -f "$CERTDIR/${domain[0]}/${domain[0]}.key" ]] || \ + [[ ! -f "$CERTDIR/${domain[0]}/${domain[0]}.csr" ]]; then + echo "The CSR or KEY for ${domain[0]} seems to be missing." + echo "Are you sure you've created the request?" + exit 1 + fi + + if [[ ! "$QUIET" == TRUE ]]; then + echo "Have you remembered to point the address" + echo "" + echo "${domain[0]}/.well-known/acme-challenge/" + echo "" + echo "to your challenges-directory? - $CHALLENGEDIR" + echo "See https://github.com/diafygi/acme-tiny#step-3-make-your-website-host"\ + "-challenge-files for more info." + echo "" + echo "If the directory isn't available at the above address, "\ + "the signing will fail." + echo "" + + read -p "Press Y to continue, or any other button to abort." -n 1 -r + if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then + echo "Okay then. Live long and prosper." + exit 1 + fi + fi + + sudo -u $LETSCAUSER -H python $ACMETINY --account-key $ACCOUNTKEY \ + --csr $CERTDIR/${domain[0]}/${domain[0]}.csr --acme-dir $CHALLENGEDIR \ + > $CERTDIR/${domain[0]}/${domain[0]}.crt + + chown $LETSCAUSER:$CERTGROUP $CERTDIR/${domain[0]}/${domain[0]}.crt + + + exit 0 + +} + + + +################################################################################ + +# main + +################################################################################ + +main() { + +cmdline ${ARGS[@]} + +} + + + +main +exit 0 diff --git a/lets-ca.sh-cron b/lets-ca.sh-cron new file mode 100644 index 0000000..da75067 --- /dev/null +++ b/lets-ca.sh-cron @@ -0,0 +1,156 @@ +#!/bin/bash +set -euo pipefail +IFS=$'\n\t' +############# + +# This script resigns certificates already in use. +# It uses lets-ca.sh to do this. + +# by Dennis Eriksen, dennis@eriksen.im, 2015-12-21 + +############# + + + + +################################################################################ + +# TODO + +################################################################################ + +# Make sure the script doesn't just loop over infinitely many certificates. It +# should probably handle ~5/week. I'm thinking it should take the five oldest, +# that are over one month old. + + + +################################################################################ + +# Config + +################################################################################ + +readonly CERTDIR="/etc/letsencrypt/certs" + +readonly LETSCASH="/usr/local/sbin/lets-ca.sh" + +readonly LOGFILE="/var/log/lets-ca.sh-cron.log" + +readonly DEBUG=FALSE + +# Time To Expiry - When do we resign certificates? +readonly TTE=5184000 # 60 days. + +# How many certs do we take each run? +readonly NUMCERTS=3 + +TMP=$(mktemp) + + + +################################################################################ + +# echo + +################################################################################ + +echo() { + [[ "$DEBUG" == TRUE ]] && builtin echo "$1" + logger -p cron.info -t lets-ca.sh-cron "$1" +} + +error() { + builtin echo "$1" + logger -p cron.err -s -t lets-ca.sh-cron "$1" +} + + + +################################################################################ + +# cleanup + +################################################################################ + +trap cleanup EXIT +trap caughterror INT TERM + +cleanup() { + rm $TMP +} + +caughterror() { + local rv=$? + cleanup + error "Script exited early. Something happened." + exit $rv +} + + + +################################################################################ + +# main + +################################################################################ + +main() { + local domain + local i=0 + + for domain in $(ls $CERTDIR); do + + # Don't do more certs than specified + if [[ $i == $NUMCERTS ]]; then + echo "\$NUMCERTS reached. $domain will have to wait." + continue + fi + + # Check if all the files are there + if [[ ! -f "$CERTDIR/$domain/$domain.key" ]] || \ + [[ ! -f "$CERTDIR/$domain/$domain.crt" ]] || \ + [[ ! -f "$CERTDIR/$domain/$domain.csr" ]]; then + error "The CRT, CSR or KEY for $domain seems to be missing." + # Let's continue the for-loop instead of aborting. + continue + fi + + # There's no need to renew certs with more than 60 days of validity left + if openssl x509 -in $CERTDIR/$domain/$domain.crt -noout -checkend $TTE; then + echo "$domain is still valid for at least another $(($TTE/60/60/24))days." + continue + fi + + # Check if there are any services specified with the cert + if [[ -f "$CERTDIR/$domain/services" ]] && \ + [[ ! -z "$CERTDIR/$domain/services" ]]; then + cat $CERTDIR/$domain/services >> $TMP + fi + + # Do the dirty deed + echo "Resigning $domain" + [[ ! "$DEBUG" == TRUE ]] && $LETSCASH -q -s $domain + echo "Deploying $domain" + [[ ! "$DEBUG" == TRUE ]] && $LETSCASH -q -d $domain + + # Number of domains handled so far. + ((i+=1)) + + done + + # Reload any services associated with the certificates (if specified) + if [[ ! -z "$TMP" ]]; then + for service in $(sort $TMP | uniq); do + echo "Reloading $service" + [[ ! "$DEBUG" == TRUE ]] && systemctl reload $service + done + fi + +} + +[[ "$DEBUG" == TRUE ]] && set -x + +main + +exit 0 |