aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rwxr-xr-xlets-ca.sh374
-rw-r--r--lets-ca.sh-cron156
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