Select Git revision
cm.sh 13.59 KiB
#!/usr/bin/env bash
# ======================================================================
#
# WRAPPER FOR ACME.SH
# Let's Encrypt client
#
# requires
# - bash
# - openssl
# - curl
# - dig (opional)
# - acme.sh client
#
# ----------------------------------------------------------------------
# 2021-02-02 <axel.hahn@iml.unibe.ch> first lines
# 2021-02-10 <axel.hahn@iml.unibe.ch> compare hashes, logging
# 2021-02-12 <axel.hahn@iml.unibe.ch> added self test
# ======================================================================
# ----------------------------------------------------------------------
#
# CONFIG
#
# ----------------------------------------------------------------------
touchfile="./log/lastchange.txt"
logfile="./log/certmanager.log"
csrfile="./templates/csr.txt"
line="_______________________________________________________________________________"
showdebug=1
writelog=1
# ----------------------------------------------------------------------
#
# INTERNAL FUNCTIONS
#
# ----------------------------------------------------------------------
# internal function; list certificates incl. creation date and renew date
function _listCerts(){
$ACME --list
}
# internal function; get a list of fqdn of all existing certs
function _listCertdomains(){
_listCerts | sed -n '2,$p' | awk '{ print $1 }'
}
# internal function; checks if a certificate for a given FQDN already exists
# used in _certMustExist, _certMustNotExist
# param string FQDN
function _certExists(){
_listCertdomains | grep "^${CM_fqdn}$" >/dev/null
}
# internal function; a certificate of a given FQDN must exist - otherwise
# the script will be aborted
# param string FQDN
function _certMustExist(){
_certExists
if [ $? -ne 0 ]; then
echo "ERROR: cert ${CM_fqdn} was not added yet."
exit 1
fi
}
# internal function; a certificate of a given FQDN must not exist - otherwise
# the script will be aborted
# param string FQDN
function _certMustNotExist(){
_certExists
if [ $? -eq 0 ]; then
echo "ERROR: cert ${CM_fqdn} was added already."
exit 1
fi
}
# internal function: transfer generated/ updated cert data to a
# known directory (based on CM_diracme - see inc_config.sh)
# used in public_add and public_renew
# used in ADD and RENEW action
function _certTransfer(){
_wd "--- acme internal data - ~/.acme.sh/${CM_fqdn}"
ls -l ~/.acme.sh/${CM_fqdn}
_wd "--- transfer acme.sh files to ${CM_dircerts}"
$ACME \
--install-cert \
-d ${CM_fqdn} \
--cert-file ${CM_outfile_cert} \
--fullchain-file ${CM_outfile_chain} \
--ca-file ${CM_outfile_ca} \
|| exit 1
# --key-file ${CM_dircerts}/${CM_fqdn}.key.pem \
_wd "--- copy key to ${CM_dircerts}"
cp ${CM_filekey} ${CM_outfile_key}
_wd "--- content of output dir $CM_dircerts:"
ls -l $CM_dircerts/*
}
# internal function; show md5 hashsums for certificate, csr and key
# for visual comparison if the match
function _certMatching(){
local md5_csr=$( test -f ${CM_filecsr} & openssl req -noout -modulus -in ${CM_filecsr} | openssl md5 | cut -f 2 -d " " )
local md5_key=$( test -f ${CM_outfile_key} & openssl rsa -noout -modulus -in ${CM_outfile_key} | openssl md5 | cut -f 2 -d " " )
local md5_cert=$( test -f ${CM_outfile_cert} & openssl x509 -noout -modulus -in ${CM_outfile_cert} | openssl md5 | cut -f 2 -d " " )
echo
echo "--- compare hashes"
echo "csr : $md5_csr (used for creation of cert)"
echo "key : $md5_key"
echo "cert : $md5_cert"
if [ "$md5_key" = "$md5_cert" ]; then
echo "OK, key and cert match :-)"
else
echo "ERROR: key and cert do NOT MATCH!"
fi
echo
}
# internal function: dig for given fqdn.
# Function stops if fqdn was not found in DNS.
# If dig is not found the function skips the DNS check.
# This function is used in _gencsr
# param string fqdn to check
function _checkDig(){
local myfqdn=$1
which dig >/dev/null
if [ $? -eq 0 ]; then
_wd "CHECK: $myfqdn exists in DNS (using dig) ..."
dig $myfqdn | grep -v '^;' | grep $myfqdn
if [ $? -ne 0 ]; then
echo "ERROR: not found. Was there a typo in the hostname??"
exit 2
fi
_wd "OK"
else
_wd "SKIP: dig was not found"
fi
echo
}
# internal function; generate a csr file before creating a new certifcate
# this function is used in public_add
function _gencsr(){
altdns=
_checkDig $CM_fqdn
for myalt in $*
do
altdns="${altdns}DNS:$myalt,"
done
altdns=$( echo $altdns | sed "s#,\$##" )
_wd "--- $CM_fqdn"
_wd "DNS alternative names: $altdns"
rm -f $CM_filecnf $CM_filekey $CM_filecsr
mkdir -p "${CM_dircsr}" 2>/dev/null
cat $csrfile \
| sed "s#__FQDN__#$CM_fqdn#g" \
| sed "s#__ALTNAMES__#$altdns#g" \
> $CM_filecnf || exit 1
# generate csr
_wd "creating key and csr"
openssl req -new -config $CM_filecnf -keyout $CM_filekey -out $CM_filecsr || exit 1
# view csr
# openssl req -noout -text -in $CM_filecsr
ls -ltr $CM_filecnf $CM_filekey $CM_filecsr
}
# internal function; check if a required 2nd CLI parameter was given
# if not the script will abort
function _requiresFqdn(){
if [ -z "$CM_fqdn" ]; then
echo "ERROR: 2nd parameter must be a FQDN for Main_Domain."
exit 1
fi
}
# internal function; it shows a message if the current instance uses a stage
# server. It shows a message that it is allowed to test arround ... or to be
# careful with LE requests on a production system
function _testStaging(){
echo $ACME_Params | grep "\-\-staging" >/dev/null
if [ $? -eq 0 ]; then
_wd "Using Let's Encrypt STAGE environment ..."
_wd "You can test and mess around. Do not use certs in production."
else
_wd "Using Let's Encrypt LIVE environment for production."
_wd "Be careful with count of connects to Let's Encrypt servers."
fi
echo
}
# set update message in a file
# param string(s) message
function _update(){
echo "[$( date )] $*" > ${touchfile}
test ${writelog} && echo "[$( date )] $*" >> ${logfile}
}
# write debug output if showdebug is set to 1
function _wd(){
test ${showdebug} && echo "DEBUG: $*"
}
# set environment for a single certificate based on FQDN
# param string FQDN
function _setenv(){
CM_fqdn=$1
CM_filecsr="${CM_dircsr}/${CM_fqdn}.csr"
CM_filecnf="${CM_dircsr}/${CM_fqdn}.cnf"
CM_filekey="${CM_dircsr}/${CM_fqdn}.key"
CM_dircerts="${CM_diracme}/${CM_fqdn}"
CM_outfile_cert=${CM_dircerts}/${CM_fqdn}.cert.cer
CM_outfile_chain=${CM_dircerts}/${CM_fqdn}.fullchain.cer
CM_outfile_key=${CM_dircerts}/${CM_fqdn}.key.pem
CM_outfile_ca=${CM_dircerts}/${CM_fqdn}.ca.cer
# echo $CM_fqdn; set | grep "^CM_"; echo
}
# ----------------------------------------------------------------------
#
# PUBLIC FUNCTIONS
#
# ----------------------------------------------------------------------
#
# pulic function ADD certificate
#
function public_add(){
_requiresFqdn
_certMustNotExist
_gencsr $CM_fqdn $*
_wd "--- create output dir $dircerts"
mkdir -p "${CM_dircerts}" 2>/dev/null
_wd "--- csr data"
$ACME --showcsr --csr $CM_filecsr || exit 1
_wd "--- create certificate"
$ACME --signcsr --csr $CM_filecsr $ACME_Params
if [ $? -ne 0 ]; then
echo "ERROR: adding cert failed. Trying to delete internal data ..."
public_delete $CM_fqdn
exit 1
fi
# $ACME --issue -d $CM_fqdn $ACME_Params || exit 1
_certTransfer
_certMatching
_update "added $CM_fqdn $*"
}
#
# pulic function ADD OR RENEW certificate
#
function public_add-or-renew(){
_requiresFqdn
_certExists
if [ $? -eq 0 ]; then
_wd "--- cert was found ... renew it (ignore --force - it comes from acme.sh)"
public_renew $*
else
_wd "--- cert does mot exist ... add it"
public_add $*
fi
}
#
# public function to delete a cert
#
function public_delete(){
_requiresFqdn
_certMustExist
# TODO: revoke it too??
# $ACME --revoke -d ${CM_fqdn}
_wd "--- delete ACME.SH data"
$ACME --remove -d ${CM_fqdn} $ACME_Params
_wd "--- delete local data"
rm -rf ${CM_dircerts} ${CM_filecnf} ${CM_filekey} ${CM_filecsr} ~/.acme.sh/${CM_fqdn}
_update "deleted ${CM_fqdn}"
}
#
# public function; list certificates incl. creation date and renew date
#
function public_list(){
_listCerts
}
#
# public function - renew a certificate
# param string fqdn of domain to renew
function public_renew(){
_requiresFqdn
_certMustExist
$ACME --renew -d ${CM_fqdn} $ACME_Params
local _rc=$?
case $_rc in
0)
_certTransfer
_certMatching
_update "renewed ${CM_fqdn}"
;;
2)
_wd "renew was skipped ... we need to wait a while."
;;
*)
_wd "Error ocured."
exit $_rc
esac
}
#
# public function - renew all certificates (to be used in a cronjob)
# no params
function public_renew-all(){
_listCertdomains | while read mydomain
do
_wd "--- renew $mydomain"
_setenv ${mydomain}
public_renew
done
}
# internal function; helper for selftest to handle a single selftest
# if a given command is successful it shows "OK" or "ERROR" followed
# by the label inparam 2.
# The value _iErrors will be incremented by 1 if an error occured.
# param string command to verify
# param string output label
function _selftestItem(){
local _check=$1
local _label=$2
local _status="OK:"
eval "$_check"
if [ $? -ne 0 ]; then
_status="ERROR: the check failed for the test of -"
_iErrors=$_iErrors+1
fi
echo "$_status $_label"
}
#
# list existing certs
# no params
function public_selftest(){
typeset -i _iErrors=0
echo
echo --- dependencies
_selftestItem "which openssl" "opemssl was found"
_selftestItem "which curl" "curl was found"
echo
echo --- acme.sh client
_selftestItem "ls -ld ${ACME}" "${ACME} exits"
_selftestItem "test -x ${ACME}" "${ACME} is executable"
echo
echo --- acme.sh installation \(may fail in future releases of acme.sh\)
_selftestItem "ls -ld ~/.acme.sh" "internal acme data were found = [acme.sh --install] was done"
_selftestItem "test -w ~/.acme.sh/" "it is writable"
echo
echo --- csr template
_selftestItem "ls -ld ${csrfile}" "csr base template exists"
_selftestItem "test -r ${csrfile}" "it is readable"
echo
echo --- output directory for csr and key
_selftestItem "ls -ld ${CM_dircsr}" "data dir for csr exists"
_selftestItem "test -w ${CM_dircsr}" "it is writable"
echo
echo --- output dir for centralized place of certificates
_selftestItem "ls -ld ${CM_diracme}" "central output dir for certificate data exists"
_selftestItem "test -w ${CM_diracme}" "it is writable"
echo
echo --- logs
_selftestItem "ls -ld ./log/" "Logdir exists"
_selftestItem "test -w" "Logdir is writable"
test -f $logfile && _selftestItem "test -w $logfile" "Logfile $logfile is writable"
test -f $touchfile && _selftestItem "test -w $touchfile" "Logfile $touchfile is writable"
echo
echo --- Errors: $_iErrors
test $_iErrors -eq 0 && echo "OK, this looks fine."
echo
exit $_iErrors
}
#
# list existing certs
# no params
function public_show(){
_requiresFqdn
_certMustExist
ls -l ${CM_filecsr} ${CM_dircerts}/*
echo $line
echo CSR $CM_filecsr
openssl req -noout -text -in $CM_filecsr | grep -E "(Subject:|DNS:)"
echo $line
echo Cert ${CM_outfile_cert}
# openssl x509 -noout -text -in ${CM_outfile_cert}
openssl x509 -noout -text -in ${CM_outfile_cert} | grep -E "(Issuer:|Subject:|DNS:)"
_certMatching
}
# ----------------------------------------------------------------------
#
# main
#
# ----------------------------------------------------------------------
cd $( dirname $0 )
cat <<ENDOFHEADER
$line
- - - ---===>>> CERT MANAGER <<<===--- - - -
$line
ENDOFHEADER
which openssl >/dev/null || exit 1
. ./inc_config.sh
if [ $? -ne 0 ]; then
echo "ERROR: loading the config failed."
echo "Copy the inc_config.sh.dist to inc_config.sh and make your settings in it."
echo
exit 1
fi
_testStaging
test -z "${CM_diracme}" && CM_diracme=./certs
test -z "${CM_dircsr}" && CM_dircsr=./csr
grep "function\ public_$1" $( basename $0 ) >/dev/null
if [ $# -gt 0 -a $? -eq 0 ]; then
# _wd $*
action=$1
CM_fqdn=$2
shift 2
test -z "${ACME}" && ACME=$( which acme.sh )
if [ ! -x "${ACME}" ]; then
echo "ERROR: acme.sh not found. You need to install acme.sh client and configure it in inc_config.sh."
exit 1
fi
_setenv $CM_fqdn
_wd "A C T I O N -->> $action <<--"
eval "public_$action $*"
else
self=$( basename $0 )
cat <<EOF
HELP
The basic syntax is
$self ACTION [FQDN] [ALIAS_1 [.. ALIAS_N]]
The ACTIONs for SINGLE certificate handlings are:
add FQDN [.. FQDN-N]
create new certificate
The first FQDN is a hostname to generate the certificate for.
Following multiple hostnames will be used as DNS aliases in the
same certificate.
It updates files in ${CM_diracme}
add-or-renew FQDN [.. FQDN-N]
This param is for automation tools like Ansible or Puppet.
It checks if the certificate for first (*) FQDN exists.
If not: add a new cert (see "add").
If so: call renew action (see "renew")
(*) it doesn't verify the DNS aliases
delete FQDN
delete all files of a given certificate
renew FQDN
renew (an already added) certificate
and update files in ${CM_diracme}
show FQDN
show place of csr + certificate data and show certificate
ACTIONs for ALL certs
list
list all certificates including creation and renew date
renew-all
renew all certificates (fast mode - without --force)
and update files in ${CM_diracme}
It is useful for a cronjob.
other ACTIONs
selftest
check of health with current setup and requirements.
This command is helpful for initial setups.
EOF
fi
echo
_testStaging