Skip to content

Instantly share code, notes, and snippets.

@AliceWonderMiscreations
Last active April 22, 2020 06:59
Show Gist options
  • Save AliceWonderMiscreations/de1a37b41df545eba3b6d6e77f6f29fb to your computer and use it in GitHub Desktop.
Save AliceWonderMiscreations/de1a37b41df545eba3b6d6e77f6f29fb to your computer and use it in GitHub Desktop.
#!/bin/bash
# RSA 3072-bit
# Tested - works for me. Line number notes assume first line is line 1
# Modify lines 85-88, 90 for your own identity (leave ${FQDN} line alone)
# Modify line 19 for your openssl/libressl binary path
# Modify line 20 for your certbot path
# Example Usage:
#
# sudo sh letsencrypt.sh example.org www.example.org support.example.org
# (all arguements need DNS records pointing to server running on)
#
# Stop web server daemon before running this script.
OPENSSL="/usr/bin/libressl"
CERTBOT="/usr/bin/certbot"
if [ ! -x ${OPENSSL} ]; then
echo "Please edit script and define your OpenSSL API implementation (line 19)."
exit 1
fi
[ "$(id -u)" != "0" ] && exit 1
FQDN="$1"
DATE="`date +%Y%m%d`"
CSR="${FQDN}-EFFLE-${DATE}.csr"
CFG="${FQDN}-EFFLE.cfg"
X509="${FQDN}-EFFLE-${DATE}.crt"
CAB="${FQDN}-EFFLE-cab-${DATE}.crt"
umask 0277
[ ! -d /etc/pki/tls/eff_private ] && mkdir -p /etc/pki/tls/eff_private
pushd /etc/pki/tls/eff_private > /dev/null 2>&1
# if existing key is less than 320 days old, use it. Otherwise generate a fresh
NEWKEY=0
keycount=`find . -type f -print |grep "^\./${FQDN}-" |wc -l`
if [ $keycount -eq 0 ]; then
NEWKEY=1
else
LATEST=`find . -type f -print |grep "^\./${FQDN}-" |tail -1 |sed -e s?"^\./"?""?`
AGE=`echo $(($(date +%s) - $(date +%s -r ${LATEST})))`
let "DAYS = ${AGE} / 86400"
if [ ${DAYS} -ge 320 ]; then
NEWKEY=1
else
PVT="${LATEST}"
fi
fi
if [ ${NEWKEY} -eq 1 ]; then
PVT="${FQDN}-EFFLE-${DATE}.key"
${OPENSSL} genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out "${PVT}"
fi
if [ ! -f "${PVT}" ]; then
echo "Something went wrong, no suitable private key"
exit 1
fi
umask 0022
popd > /dev/null 2>&1
# generate CSR
[ ! -d /etc/pki/tls/csr ] && mkdir /etc/pki/tls/csr
pushd /etc/pki/tls/csr > /dev/null 2>&1
[ -f "${CFG}" ] && rm -f "${CFG}"
[ -f "${CSR}" ] && rm -f "${CSR}"
cat <<EOF > "${CFG}"
[req]
distinguished_name = req_distinguished_name
req_extensions = ext
prompt = no
[ req_distinguished_name ]
C = YourCountryCode
ST = YourState
L = YourCity
O = Your Organization Name
CN = ${FQDN}
emailAddress = user@example.org
[ext]
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature
extendedKeyUsage = serverAuth,clientAuth
subjectAltName = @san
[san]
EOF
COUNTER=0
for arg in $@; do
((COUNTER++))
echo "DNS.${COUNTER} = ${arg}" >> "${CFG}"
done
${OPENSSL} req -new -key "../eff_private/${PVT}" -out "${CSR}" -config "${CFG}"
if [ $? -ne 0 ]; then
echo "Problem creating CSR"
exit 1
fi
popd > /dev/null 2>&1
if [ ${NEWKEY} -eq 1 ]; then
echo "New Private Key Generated: /etc/pki/tls/eff_private/${PVT}"
fi
echo "CSR file: /etc/pki/tls/csr/${CSR}"
if [ -x ${CERTBOT} ]; then
[ ! -d /etc/pki/tls/eff_certs ] && mkdir -p /etc/pki/tls/eff_certs
${CERTBOT} certonly --standalone --csr /etc/pki/tls/csr/${CSR} \
--cert-path /etc/pki/tls/eff_certs/${X509} \
--chain-path /etc/pki/tls/eff_certs/${CAB}
fi
if [ -f "/etc/pki/tls/eff_certs/${X509}" ]; then
pushd /etc/pki/tls/eff_certs/
# generate DANE
FINGERPRINT="`${OPENSSL} x509 -noout -fingerprint -sha256 < "${X509}" |tr -d : |cut -d"=" -f2`"
echo ""
echo "TLSA from Cert:"
echo "3 0 1 ${FINGERPRINT}"
echo ""
echo "TLSA from PubKey:"
FINGERPRINT="`${OPENSSL} x509 -in ${X509} -noout -pubkey \
|${OPENSSL} pkey -pubin -outform DER \
|${OPENSSL} dgst -sha256 -binary \
|hexdump -ve '/1 "%02x"'`"
FINGERPRINT=${FINGERPRINT^^}
echo "3 1 1 ${FINGERPRINT}"
fi
exit 0
@AliceWonderMiscreations
Copy link
Author

subjectAltName not tested, I'll make sure that works in a day or two.

@AliceWonderMiscreations
Copy link
Author

subjectAltName is properly working.

@siarsky
Copy link

siarsky commented Apr 22, 2020

Hi @AliceWonderMiscreations,

I like your approach of keeping Apache/key parameters configuration separated from letsencrypt process. I've adapted your script as a cronjob script. User does not need to think.

Feel free to use the version in PS/part of it or just keep it here for the others.

Thank you, a nice idea!
Brani

PS:


#!/bin/bash
#
# Source: https://gist.github.com/AliceWonderMiscreations/de1a37b41df545eba3b6d6e77f6f29fb
# See thread: https://community.letsencrypt.org/t/use-existing-private-key/60182
#
# April/2018 Alice Wonder      - Original script
# April/2020 Branislav Siarsky - Adapted as a cronjob script, used/tested on Debian 10 buster
#
# This script is written as a cronjob script. Run it once a day from a crontab. 
# It verifies if key and certificate fulfil differently configurable expiration dates.
# If key/certificate is not expired nothing is done.
# If the certificate or key needs to be regenerated or they do not exist yet:
# 1. This script generates key (if expires/not existing) and certificate request
# 2. Stops apache
# 3. Calls letsencrypt
# 4. If a new certificate was properly generated
#       Script makes backup of old key/certificate (if exists)
#       Script installs new key and certificate
# 5. If a new certificate was not generated (due to network problems etc.)
#       Script deletes own temporary files
#       Existing key and certificate stay untouched in place
# 6. Starts apache
#
# Example usage. Call as root:
# letsencrypt.sh example.org www.example.org support.example.org
#
# All DNS domains (example.org www.example.org support.example.org) need a DNS record pointing to the server where this script is running on.
# The first domain (example.org) is used as the primary (FQDN), key and certificate files will get FQDN name (example.org.pem, example.org.key).
# Do not forget to adapt your apache SSL configuration.
#
# The other domains (www.example.org support.example.org) are added into the same certificate into DNS certificate request section.
#
# If you need to reinforce certificate generation (you want the script to ignore configured expiration parameters), use "--force" option.
# Be aware of letsencrypt limits, if you use "--force" too often you will be blocked for some time.
# Use letsencrypt test system if you need to test adaptions of this script.
#
# What you might want to change (search for a section with this label in this script):
# - System/script parameters
# - Private key parameters
#
# What you definitely want to change
# - Certificate parameters
#

print_cert_hashes() 
{
if [ "${PRINT_CERT_HASHES}" == "1" ]; then
  if [ -f "${CERT_DIR}/${X509}" ]; then
    # generate DANE
    FINGERPRINT="`${OPENSSL} x509 -noout -fingerprint -sha256 < "${CERT_DIR}/${X509}" | tr -d : | cut -d"=" -f2`"
    echo ""
    echo "TLSA from Cert:"
    echo "3 0 1 ${FINGERPRINT}"
    echo "TLSA from PubKey:"
    FINGERPRINT="`${OPENSSL} x509 -in ${X509} -noout -pubkey \
      | ${OPENSSL} pkey -pubin -outform DER \
      | ${OPENSSL} dgst -sha256 -binary \
      | hexdump -ve '/1 "%02x"'`"
    FINGERPRINT=${FINGERPRINT^^}
    echo "3 1 1 ${FINGERPRINT}"
  fi
fi
}

#delete all temporary files
delete_temp_files()
{
FILE="${CSR_DIR}/${CFG}"
rm ${FILE} >/dev/null 2>&1
FILE="${CSR_DIR}/${CSR}"
rm ${FILE} >/dev/null 2>&1
FILE="${PK_DIR}/${PK}"
rm ${FILE} >/dev/null 2>&1
FILE="${CERT_DIR}/${X509}"
rm ${FILE} >/dev/null 2>&1
FILE="${CERT_DIR}/${CAB}"
rm ${FILE} >/dev/null 2>&1
}

#System/script parameters
OPENSSL="/usr/bin/openssl"
CERTBOT="/usr/bin/certbot"
PK_DIR="/etc/ssl/private"
CSR_DIR="/etc/ssl/certs"
CERT_DIR="/etc/ssl/certs"
APACHE_INITD="/etc/init.d/apache2"

#Private key parameters
PK_OWNER=":ssl-cert"        #set owner of private key, if needed
PK_UMASK="0640"             #set umask of private key, if needed
PK_TYPE="RSA"               #generate a RSA key type
PK_SIZE="3072"              #generate a key of this size
PK_MAX_AGE="320"            #generate new private key if it is elder than X days
CERT_RENEW_BEFORE="5"       #renew certificate X days before expires
PRINT_CERT_HASHES="0"       #set to 1 if you use DNSSEC or DANE and needs certificate hashes

SCRIPT_NAME=`basename "$0"`
if [ "$(id -u)" != "0" ]; then
  echo "Start ${SCRIPT_NAME} with root privileges"
  exit 1
fi

FORCE_RUN=0
if [ "$1" == "--force" ]; then
  FORCE_RUN=1
  shift
fi

if [ "$1" == "" ]; then
  echo "Usage: ${SCRIPT_NAME} {--force} domain1 domain2 ... domainX"
  echo "       --force : ignore age of certificate and private key and force calling letsencrypt"
  echo "Example: ${SCRIPT_NAME} example.com www.example.com monitor.example.com"
  exit 1
fi

if [ ! -x ${OPENSSL} ]; then
  echo "OpenSSL command not set"
  exit 1
fi

if [ ! -d ${PK_DIR} ]; then
  echo "Private key directory ${PK_DIR} does not exist"
  exit 1
fi

if [ ! -d ${CSR_DIR} ]; then
  echo "CSR directory ${CSR_DIR} does not exist"
  exit 1
fi

if [ ! -d ${CERT_DIR} ]; then
  echo "Certificates directory ${CERT_DIR} does not exist"
  exit 1
fi

FQDN="$1"
DATE="`date +%Y%m%d`"

#Files generated by this script
DATE="`date +%Y%m%d`"
CFG="${FQDN}.cfg.${DATE}"  #template for certificate request (contains data like CN)
PK="${FQDN}.key.${DATE}"   #private key
CSR="${FQDN}.csr.${DATE}"  #certificate request

#Files generated by certbot
X509="${FQDN}.pem.${DATE}" #signed certificate 
CAB="${FQDN}.cab.${DATE}"  #chain path

# if existing key is less than ${PK_MAX_AGE} days old, use it. Otherwise generate a new one
NEED_NEWPK=1
if [ $FORCE_RUN -eq 0 ]; then
  COUNT=`cd ${PK_DIR}; find . -type f -name "${FQDN}.key" | wc -l`
  if [ $COUNT -gt 0 ]; then
    PK_LAST=`cd ${PK_DIR}; find . -type f -name "${FQDN}.key" | xargs basename`
    PK_LAST_AGE=`echo $(($(date +%s) - $(date +%s -r ${PK_DIR}/${PK_LAST})))`
    let "SECONDS = ${PK_MAX_AGE} * 86400"
    if [ ${PK_LAST_AGE} -le ${SECONDS} ]; then
      #reuse old private key
      cp -p "${PK_DIR}/${PK_LAST}" "${PK_DIR}/${PK}"
      NEED_NEWPK=0
    fi
  fi
fi

# check if certificate exists and if so, if it expires in ${CERT_RENEW_BEFORE} days
NEED_NEWCSR=1
if [ $FORCE_RUN -eq 0 ]; then
  FILE_DATUM="${CERT_DIR}/${X509}"
  FILE="${FILE_DATUM%.*}"
  if [ -f "${FILE}" ]; then
    let "SECONDS = ${CERT_RENEW_BEFORE} * 86400"
    ${OPENSSL} x509 -checkend ${SECONDS} -noout -in "${FILE}" >/dev/null 2>&1
    if [ $? -eq 0 ]; then
      #Certificate is not going to expire in next ${CERT_RENEW_BEFORE} days, no renewal needed
      NEED_NEWCSR=0
    fi
  fi
fi

if [ "${NEED_NEWCSR}" == "0" -a "${NEED_NEWPK}" == "0" ]; then
  echo "${FQDN}: Certificate and private key are up to date, nothing to do"
  delete_temp_files
  print_cert_hashes
  exit 1
fi

if [ ${NEED_NEWPK} -eq 1 ]; then
  echo "Generating new key ${PK}"
  ${OPENSSL} genpkey -algorithm ${PK_TYPE} -pkeyopt rsa_keygen_bits:${PK_SIZE} -out "${PK_DIR}/${PK}"
  if [ "${PK_OWNER}" != "" ]; then
    chown "${PK_OWNER}" "${PK_DIR}/${PK}"
  fi
  if [ "${PK_UMASK}" != "" ]; then
    chmod "${PK_UMASK}" "${PK_DIR}/${PK}"
  fi
else 
  echo "Using existing key ${PK_DIR}/${PK}"
fi

if [ ! -f "${PK_DIR}/${PK}" ]; then
  echo "Something went wrong, no suitable private key"
  exit 1
fi

# generate CSR
rm -f ${CSR_DIR}/${CFG} >/dev/null 2>&1
rm -f ${CSR_DIR}/${CSR} >/dev/null 2>&1

#Certificate parameters
cat <<EOF > "${CSR_DIR}/${CFG}"
[req]
distinguished_name     = req_distinguished_name
req_extensions         = ext
prompt                 = no
[ req_distinguished_name ]
C                      = CH
ST                     = Basel Stadt
L                      = Basel
O                      = Smartdone GmbH
CN                     = ${FQDN}
emailAddress           = hostmaster@${FQDN}
[ext]
basicConstraints       = critical,CA:FALSE
keyUsage               = critical,digitalSignature
extendedKeyUsage       = serverAuth,clientAuth
subjectAltName         = @san
[san]
EOF

COUNTER=0
for arg in $@; do
  ((COUNTER++))
  echo "DNS.${COUNTER}                  = ${arg}" >> "${CSR_DIR}/${CFG}"
done

echo "Generating CSR"
${OPENSSL} req -new -key "${PK_DIR}/${PK}" -out "${CSR_DIR}/${CSR}" -config "${CSR_DIR}/${CFG}"
if [ $? -ne 0 ]; then
  echo "Problem creating CSR"
  delete_temp_files
  exit 1
else
  echo "CSR file ${CSR_DIR}/${CSR} generated"
fi

if [ -x ${CERTBOT} ]; then
  echo "Stopping Apache"
  ${APACHE_INITD} stop
  ${CERTBOT} certonly --standalone --csr ${CSR_DIR}/${CSR} \
                      --cert-path ${CERT_DIR}/${X509} \
                      --chain-path ${CERT_DIR}/${CAB}
  RC="$?"
  if [ ${RC} -eq 0 -a -f ${CERT_DIR}/${X509} -a -f ${CERT_DIR}/${CAB} ]; then
    echo "CSR signing OK"
    
    FILE_DATUM="${PK_DIR}/${PK}"
    FILE="${FILE_DATUM%.*}"
    echo "Backuping ${FILE}"
    mv ${FILE} ${FILE}.bak >/dev/null 2>&1
    mv ${FILE_DATUM} ${FILE} >/dev/null 2>&1

    FILE_DATUM="${CERT_DIR}/${X509}"
    FILE="${FILE_DATUM%.*}"
    echo "Backuping ${FILE}"
    mv ${FILE} ${FILE}.bak >/dev/null 2>&1
    mv ${FILE_DATUM} ${FILE} >/dev/null 2>&1

    FILE_DATUM="${CERT_DIR}/${CAB}"
    FILE="${FILE_DATUM%.*}"
    echo "Backuping ${FILE}"
    mv ${FILE} ${FILE}.bak >/dev/null 2>&1
    mv ${FILE_DATUM} ${FILE} >/dev/null 2>&1
  else
    echo "Problem with signing of CSR"
    if [ ${RC} -ne 0 ]; then
      echo "  ${CERTBOT} return code ${RC}"
    fi
    FILE="${CERT_DIR}/${X509}"
    if [ ! -f ${FILE} ]; then
      echo "  ${FILE} not created"
    fi
    FILE="${CERT_DIR}/${CAB}"
    if [ ! -f ${FILE} ]; then
      echo "  ${FILE} not created"
    fi
  fi
  ${APACHE_INITD} start
fi

delete_temp_files
print_cert_hashes

exit 0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment