#!/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 # 2021-02-17 <axel.hahn@iml.unibe.ch> ensure checks list of aliases; new: optional host filter before adding a cert # 2021-03-12 <axel.hahn@iml.unibe.ch> create file for haproxy # 2021-03-22 <axel.hahn@iml.unibe.ch> for haproxy: use chained cert instead of server cert # 2021-04-12 <axel.hahn@iml.unibe.ch> reject multiple usage of fqdn in cli params # 2021-04-12 <axel.hahn@iml.unibe.ch> optional: force excecution with a given user only # 2021-06-24 <axel.hahn@iml.unibe.ch> added transfer command; delete files if acme.sh --install-cert ... failes # 2021-07-14 <axel.hahn@iml.unibe.ch> added _wait_for_free_slot in cert actions to execute multiple processes sequentially # 2021-09-27 <axel.hahn@iml.unibe.ch> softer behaviour: do not revoke changed certs (add does not stop; ensure does not delete) # 2021-12-23 <axel.hahn@iml.unibe.ch> added param --trace as 1st param to generate a trace log # 2022-01-10 <axel.hahn@iml.unibe.ch> _wait_for_free_slot: exclude ssh calls # 2022-03-30 <axel.hahn@iml.unibe.ch> remove usage of csr and generation of key file # 2022-03-31 <axel.hahn@iml.unibe.ch> dns authentication with alias domain # 2022-04-04 <axel.hahn@iml.unibe.ch> Bugfix: copy key from csr folder to ~/.acme/ # 2022-04-04 <axel.hahn@iml.unibe.ch> added param "list-old" # 2022-04-07 <axel.hahn@iml.unibe.ch> fix missing key in public_ensure before calling public_add too. # 2022-04-20 <axel.hahn@iml.unibe.ch> fix multiple domains using domain alias # 2022-04-21 <axel.hahn@iml.unibe.ch> mix multiple domains using domain alias or not # 2022-05-19 <axel.hahn@iml.unibe.ch> add timer and debug.log # 2022-05-20 <axel.hahn@iml.unibe.ch> update _wait_4_free_slot and data in debug.log # 2023-02-01 <axel.hahn@unibe.ch> skip function _fixKeyfile with former workaround # 2023-05-08 <axel.hahn@unibe.ch> fix: "key and cert do not match" # 2024-03-21 <axel.hahn@unibe.ch> shorter sleep times # 2025-01-08 <axel.hahn@unibe.ch> support for http challenge on port 80; more params supported for force renew, http challenge # ====================================================================== # ---------------------------------------------------------------------- # # CONFIG # # ---------------------------------------------------------------------- _version="2025-01-08" logdir="./log" touchfile="$logdir/lastchange.txt" logfile="$logdir/certmanager.log" debuglogfile="$logdir/debug.log" # CSR USAGE WAS REMOVED # csrfile="./templates/csr.txt" line="_______________________________________________________________________________" # flag: show debug infos on console (STDOUT) CM_showdebug=0 # flag: write a log for created/ renewd/ deleted certs CM_writelog=1 # flag: write a log for executed functions with timer and process count CM_writedebuglog=0 CM_timer_start=$( date +%s.%N ) # webroot to write challenge files into - CM_webroot="" # force renew (default: disabled - remember quotas on LE) CM_force=0 # ---------------------------------------------------------------------- # # INTERNAL FUNCTIONS # # ---------------------------------------------------------------------- # BUGFIX: acme.sh does not create a new key file on renew. # After switching from csr method to param -d we got a 0 byte Keyfile function _fixKeyfile(){ echo "SKIP: _fixKeyfile won't be executed anymore." # local _acme_keyfile=~/.acme.sh/${CM_fqdn}/${CM_fqdn}.key # if test ! -f "$_acme_keyfile" # then # echo "FIX: copy key from csr folder $CM_filekey to $_acme_keyfile" # if ! cp "$CM_filekey" "$_acme_keyfile" # then # exit 1 # fi # fi } # 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 $ACME --info -d "${CM_fqdn}" 2>/dev/null | grep "letsencrypt.org" >/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 _we "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(){ if _certExists then _we "Cert ${CM_fqdn} was added already." # exit 1 echo "Press Ctrl+C to abort within the next 3 sec..." sleep 3 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 "--- delete current files in ${CM_dircerts}/ if they already exist." test -d ${CM_dircerts} && rm -f "${CM_dircerts}/*" 2>/dev/null _wd "--- transfer acme.sh files to ${CM_dircerts}/" if ! $ACME \ --install-cert \ -d "${CM_fqdn}" \ --key-file "${CM_outfile_key}" \ --cert-file "${CM_outfile_cert}" \ --fullchain-file "${CM_outfile_chain}" \ --ca-file "${CM_outfile_ca}" then echo "ERROR occured during acme transfer. Removing files in ${CM_dircerts} to prevent strange effects..." rm -f "${CM_dircerts}/*" exit 2 fi echo "OK." # _wd "--- copy key to ${CM_dircerts}" # cp ${CM_filekey} ${CM_outfile_key} _wd "--- create chained file for haproxy" cat "${CM_outfile_chain}" "${CM_outfile_key}" > "${CM_outfile_haproxy}" _wd "--- content of output dir $CM_dircerts:" if ! ls -l "${CM_outfile_cert}" "${CM_outfile_chain}" "${CM_outfile_key}" "${CM_outfile_haproxy}" then echo "ERROR missing a file (or no access?)" rm -f "${CM_dircerts}/*" exit 2 fi } # internal function; show md5 hashsums for certificate and key # for visual comparison if the match function _certMatching(){ # CSR USAGE WAS REMOVED # local md5_csr=$( test -f ${CM_filecsr} && openssl req -noout -modulus -in ${CM_filecsr} | openssl md5 | cut -f 2 -d " " ) local md5_key; md5_key=$( test -f ${CM_outfile_key} && openssl rsa -noout -modulus -in ${CM_outfile_key} | openssl md5 | cut -f 2 -d " " ) local md5_cert; 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" # CSR USAGE WAS REMOVED # 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 _we "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 _dnsCheck # param string fqdn to check # param string type of dns entry; one of a|cname # param string optional filter on output of dig (regex) function _checkDig(){ local myfqdn=$1 local _type=${2:-"a"} local _verify=${3:-"."} if which dig >/dev/null then # _wd "[$myfqdn] exists as type [$_type] in DNS?" if ! dig "${myfqdn}" "${_type}" | grep "^${myfqdn}" | grep -E "${_verify}" then _we "[$myfqdn] was not found. Maybe there is a typo in the hostname or it does not exist in DNS." exit 2 fi _wd "OK: [$myfqdn] exists in DNS." else _wd "SKIP: dig was not found" fi echo } # internal function: check DNS entries # - the hostname to be added in the certificate must exist # - if a hostname does not match and CM_challenge_alias was set: # - _acme-challenge.FQDN must be a cname to _acme-challenge.${CM_challenge_alias} # Function stops if a fqdn was not found in DNS. # param string fqdn(s) that are part of the certificate function _dnsCheck(){ local altdns= local _mydomain= local _subdomain='_acme-challenge' for _mydomain in $* do _wd "dig check - domain for cert" _checkDig "$_mydomain" "a" "IN.*(A|CNAME)" # matches A and CNAME records if [ -n "${CM_challenge_alias}" ] && ! echo "$_mydomain" | grep "${CM_certmatch}" >/dev/null then _wd "dig check - cname ${_subdomain}.${_mydomain} must exist" _checkDig "${_subdomain}.${_mydomain}" "cname" _wd "dig check - cname ${_subdomain}.${_mydomain} must point to ${_subdomain}.${CM_challenge_alias}" _checkDig "${_subdomain}.${_mydomain}" "cname" "${_subdomain}.${CM_challenge_alias}" fi done } # CSR USAGE WAS REMOVED # internal function; generate a csr file before creating a new certifcate # this function is used in public_add # function _UNUSED_gencsr(){ # local altdns= # 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; get a sorted list of DNS aliases in the current cert function _getAliases(){ _sortWords "$( openssl x509 -noout -text -in ${CM_outfile_cert} \ | grep -E "(DNS:)" \ | sed "s#^ *##g" \ | sed "s#DNS:##g" \ | sed "s#,##g" )" } # internal function; check if a required 2nd CLI parameter was given # if not the script will abort function _requiresFqdn(){ if [ -z "$CM_fqdn" ]; then _we "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(){ if echo "${ACME_Params}" | grep -- "--staging" >/dev/null; 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 } # internal function; if a user was set as CM_user then verify it with # current user function _testUser(){ if [ ! -z "$CM_user" ]; then local _sUser _sUser="$( id | cut -f 2 -d "(" | cut -f 1 -d ")")" if [[ $_sUser != "$CM_user" ]]; then _we "Run this script under user [$CM_user] - not as $_sUser." exit 1 fi fi } # set update message into access log file # global bool CM_writedebuglog flag to write access log. # param string(s) message function _debuglog(){ if [ ${CM_writedebuglog} -eq 1 ]; then echo "$( date ) $CM_fqdn [$$] | $(show_timer) | $*" >> ${debuglogfile} fi } # set update message in a file # param string(s) message function _update(){ echo "[$( date )] $*" > ${touchfile} test ${CM_writelog} -ne 0 && echo "[$( date )] $*" >> ${logfile} } # "neverending" loop that waits until the current process is # the one with lowest PID function _wait_for_free_slot(){ local _bWait=true _debuglog "start in _wait_for_free_slot" typeset -i local _iFirstPID=0 typeset -i local _iPos=0 local _sProcesses _wd "--- Need to wait until own process PID $$ is on top ... " while [ $_bWait = true ]; do _sProcesses=$( ps -ef | grep "bash.*$0" | grep -v "ssh.*@" | grep -v "grep" | sort -k 2 -n ) _iPos=$( echo "$_sProcesses" | grep -n " $$ " | head -1 | cut -f 1 -d ':' ) _wd "instances: $_iProcesses" test ${CM_showdebug} -ne 0 && echo "$_sProcesses" # if [ $_iFirstPID -eq $$ ]; then if [ $_iPos -eq 1 ]; then _bWait=false _debuglog "GO from _wait_for_free_slot" _wd "OK. Go!" else _iProcesses=$( echo "$_sProcesses" | wc -l ) _iFirstPID=$( echo "$_sProcesses" | head -1 | awk '{ print $2}' ) _debuglog "zzz ... waiting in _wait_for_free_slot ... $_iFirstPID is first ... my pos is $_iPos of $_iProcesses" sleep $((3 + RANDOM % 3)); fi done _debuglog "end _wait_for_free_slot" } # write debug output if CM_showdebug is set to 1 # param string message (prefix "DEBUG" will be added in front) function _wd(){ test ${CM_showdebug} -ne 0 && echo -e "\e[1;30mDEBUG: $* \e[0m" } # show error message; # param string message (prefix "ERROR" will be added in front) function _we(){ echo -e "\e[1;37;41mERROR: $*\e[0m" } # set environment for a single certificate based on FQDN # param string FQDN function _setenv(){ CM_fqdn=$1 # CSR USAGE WAS REMOVED # keeping vars to delete files of existing certs that used a csr 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_haproxy=${CM_dircerts}/${CM_fqdn}.haproxy.pem CM_outfile_ca=${CM_dircerts}/${CM_fqdn}.ca.cer # echo $CM_fqdn; set | grep "^CM_"; echo } # internal function; helper: sort words in alphabetic order function _sortWords(){ echo "$*" | tr " " "\n" | sort | tr "\n" " " } # internal function; verify fqdn in cli params - each fqdn is allowed only once. # on error it shows the count of usage of each fqdn function _testFqdncount(){ typeset -i local iHostsInParam=$( echo $* | wc -w ) typeset -i iHostsUniq=$( echo $* | tr " " "\n" | sort -u | wc -w ) if [ $iHostsInParam -ne $iHostsUniq ]; then _we "Each given FQDN is allowed only once. You need to remove double entries." for myhost in $( echo $* | tr " " "\n" | sort -u ) do typeset -i iHostcount=$( echo $* | tr " " "\n" | grep "^$myhost$" | wc -l ) test $iHostcount -gt 1 && echo " $iHostcount x $myhost" done echo exit 1 fi } # get time in sec and milliseconds since start # no parameter is required function show_timer(){ local timer_end; timer_end=$( date +%s.%N ) local totaltime; totaltime=$( awk "BEGIN {print $timer_end - $CM_timer_start }" ) local sec_time; sec_time=$( echo "$totaltime" | cut -f 1 -d "." ) test -z "$sec_time" && sec_time=0 local ms_time; ms_time=$( echo "$totaltime" | cut -f 2 -d "." | cut -c 1-3 ) echo "$sec_time.$ms_time sec" } # show help text function show_help(){ self="$( basename "$0" )" cat <<EOF HELP Wrapper script for acme.sh to handle certificates. For automation you should use the "ensure" action that detects if a certificate must be created, renewed or re-created. 📄 Source: <https://git-repo.iml.unibe.ch/iml-open-source/iml-certman> 📜 License: GNU GPL 3.0 📗 Docs: <https://os-docs.iml.unibe.ch/iml-certman/> SYNTAX: $self [OPTIONS] ACTION <FQDN> [<ALIASES>] OPTIONS: -a|--alias Use http challenge with existing http server on port 80 Challenge file will be written into ../alias-dir/ -f|--force Force renew of certificate even if it is not due yet. Use it carefully - remember the execution limits on Let's Encrypt. -t|--trace the output additionally will be written into a tracelog file below $logdir. -v|--verbose show debug infos on console. Remark: for permanent usage set CM_showdebug=1 in inc_config.sh -w|--webroot <DIR> Use http challenge with existing http server on port 80 Challenge file will be written into given directory 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} ensure <FQDN> [.. <FQDN-N>] It ensures that a certificate with given aliases exists and is up to date. This param is for simple usage in automation tools like Ansible or Puppet. It is required to add all aliases as parameters what is unhandy for direct usage on cli. If the cert does not exist it will be created (see "add"). If fqdn and aliases are the same like in the certificate it performs a renew. If fqdn and aliases differ: - the current certificate will be rejected + deleted (see "delete") - a new certificate will be added () 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 certificate data and show basic certificate data (issuer, subject, aliases, ending date) transfer <FQDN> Transfer cert from acme.sh internal cache to our output dir again. It is done during add or renew. With transfer command you can repeat it. ACTIONs for ALL certs list list all certificates including creation and renew date list-old list all certificates older 65 and older 90 days and exit. Exitcodes: 0 - all certs are up to date. 1 - certificates to renew were found 2 - outdatedt certificates were found 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 } # ---------------------------------------------------------------------- # # PUBLIC FUNCTIONS # # ---------------------------------------------------------------------- # # pulic function ADD certificate # function public_add(){ local _params="" _debuglog "start public_add" _wait_for_free_slot _requiresFqdn _certMustNotExist _dnsCheck $CM_fqdn $* local iCounter typeset -i iCounter=0 _wd "--- create output dir $dircerts" mkdir -p "${CM_dircerts}" 2>/dev/null _wd "--- issue certificate" for _mydomain in $CM_fqdn $* do iCounter+=1 _params+="-d $_mydomain " if [ -n "$CM_webroot" ] then echo "INFO: Using http request on port 80 as challenge; put file into $CM_webroot" if [ $iCounter -eq 1 ] then _params+="-w $CM_webroot " fi else if [ -n "${CM_challenge_alias}" ] then echo "INFO: Using DNS for challenge" _params+=" --challenge-alias " if ! echo "$_mydomain" | grep "${CM_certmatch}" >/dev/null then _params+="${CM_challenge_alias} " else _params+="no " fi fi fi done # 2023-05-08: Specifies the domain key length _params+="--keylength 2048 " echo "$ACME --issue $_params $ACME_Params" if ! $ACME --issue $_params $ACME_Params then _we "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 $*" _debuglog "end public_add" } # CSR USAGE WAS REMOVED # function OLD__public_add(){ # _wait_for_free_slot # _requiresFqdn # _certMustNotExist # for myhost in $( echo $CM_fqdn $*) # do # echo $myhost | grep "$CM_certmatch" >/dev/null # if [ $? -ne 0 ]; then # echo "ERROR: host $myhost does not match [$CM_certmatch]." # exit 1 # fi # done # _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" # echo $ACME --signcsr --csr $CM_filecsr $ACME_Params # $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_ensure(){ _wait_for_free_slot _requiresFqdn _certExists if [ $? -eq 0 ]; then _wd "--- cert $CM_fqdn was found ... compare aliases" local _newAliases; _newAliases=$( _sortWords $CM_fqdn $* ) local _certAliases; _certAliases=$( _getAliases ) _wd "from params: $_newAliases" _wd "inside cert: $_certAliases" if [ "$_newAliases" = "$_certAliases" ]; then _wd "--- DNS aliases match ... renew it (ignore --force - it comes from acme.sh)" public_renew $* else # _wd "--- DNS aliases do NOT match ... deleting cert and create a new one" # public_delete $* _wd "--- DNS aliases do NOT match ... creating a new one" _fixKeyfile public_add $* fi else _wd "--- cert does mot exist ... add it" public_add $* fi } # # public function to delete a cert # function public_delete(){ _debuglog "start public_delete" _wait_for_free_slot _requiresFqdn _certMustExist # TODO: revoke it too?? _wd "--- revoke cert" $ACME --revoke -d ${CM_fqdn} $ACME_Params _wd "--- delete ACME.SH data" $ACME --remove -d ${CM_fqdn} $ACME_Params _wd "--- delete local data" # CSR USAGE WAS REMOVED rm -rf ${CM_dircerts} ${CM_filecnf} ${CM_filekey} ${CM_filecsr} ~/.acme.sh/${CM_fqdn} && echo OK _update "deleted ${CM_fqdn}" _debuglog "end public_delete" } # # public function; list certificates incl. creation date and renew date # function public_list(){ _listCerts } # # public function; list old / outdated certificates # function public_list-old(){ local _iRuntime=90 local _iWarn=65 local _rc; typeset -i _rc=0 cd "${CM_dircerts}" || exit 1 echo echo "Cert dir is $( pwd )" echo echo "---- Certificates expiring soon - with age $_iWarn ... $_iRuntime days:" if find -type f -name "*.cert.cer" -mtime +$_iWarn -mtime -$_iRuntime | grep . >/dev/null then find -type f -name "*.cert.cer" -mtime +$_iWarn -mtime -$_iRuntime -exec ls -ld {} \; | nl _rc=1 else echo " NONE." fi echo echo "---- Certificate list ... older $_iRuntime days:" if find -type f -name "*.cert.cer" -mtime +$_iRuntime | grep . >/dev/null then find -type f -name "*.cert.cer" -mtime +$_iRuntime -exec ls -ld {} \; | nl _rc=2 else echo " NONE." fi echo cd - >/dev/null _wd "Exiting with rc=$_rc" exit $_rc } # # public function - renew a certificate # param string fqdn of domain to renew function public_renew(){ _debuglog "start public_renew" _wait_for_free_slot _requiresFqdn _certMustExist _fixKeyfile if [ $CM_force -ne 0 ]; then _wd "renew was forced" $ACME --force --renew -d "${CM_fqdn}" $ACME_Params else _wd "soft renew" $ACME --renew -d "${CM_fqdn}" $ACME_Params fi 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 _debuglog "end public_renew" } # # 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" "openssl 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 # CSR USAGE WAS REMOVED # 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 # CSR USAGE WAS REMOVED # ls -l ${CM_filecsr} ${CM_dircerts}/* ls -l ${CM_dircerts}/* _certMatching # CSR USAGE WAS REMOVED # echo $line # echo CSR $CM_filecsr # openssl req -noout -text -in $CM_filecsr | grep -E "(Subject:|DNS:)" | sed "s#^\ *##g" for myfile in ${CM_outfile_cert} ${CM_outfile_haproxy} do echo $line echo Cert ${myfile} # openssl x509 -noout -text -in ${CM_outfile_cert} openssl x509 -noout -text -in ${myfile} | grep -E "(Issuer:|Subject:|Not\ |DNS:)"| sed "s#^\ *##g" done } # Transfer cert from acme.sh internal cache to our output dir again function public_transfer(){ _wait_for_free_slot _requiresFqdn _certExists _certTransfer } # ---------------------------------------------------------------------- # # main # # ---------------------------------------------------------------------- cd $( dirname $0 ) test -z "${CM_diracme}" && CM_diracme=./certs test -z "${CM_dircsr}" && CM_dircsr=./csr cat <<ENDOFHEADER $line - - - ---===>>> CERT MANAGER - v$_version <<<===--- - - - $line ENDOFHEADER . ./inc_config.sh if [ $? -ne 0 ]; then _we "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 while [[ "$#" -gt 0 ]]; do case $1 in -a|--alias) cd .. CM_webroot="$( pwd )/alias-dir" cd - echo "INFO: directory for challenges is [$CM_webroot]" shift ;; -f|--force) echo "INFO: enable --force (for renewal)" CM_force=1 exit 0 ;; -h|--help) show_help exit 0 ;; -t|--trace) tracelog="$logdir/trace__$2__$3__$(date +%Y-%m-%d__%H-%M-%S).log" exec > >(tee -a "${tracelog}" ) exec 2> >(tee -a "${tracelog}" >&2) echo "TRACELOG was triggered." echo "TIME : $(date)" echo "COMMAND: $0 $*" echo "LOG : $tracelog" # set -vx shift 1 ;; -v|--verbose) CM_showdebug=1 shift 1 ;; -w|--webroot) CM_webroot="$2" echo "INFO: directory for challenges is [$CM_webroot]" # ACME="sudo $ACME" # shift shift ;; *) if grep "^-" <<< "$1" >/dev/null ; then echo; _we "Unknown parameter: $1"; show_help; exit 2 fi break; ;; esac; done which openssl >/dev/null || exit 1 _testUser _testStaging grep "function public_$1" "$( basename "$0" )" >/dev/null if [ $# -gt 0 -a $? -eq 0 ]; then # _wd $* action=$1 CM_fqdn=$2 shift 2 _testFqdncount "$CM_fqdn" $* test -z "${ACME}" && ACME=$( which acme.sh ) if [ ! -x "${ACME}" ]; then _we "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 <<--" _debuglog ">>> START public_$action $CM_fqdn $*" eval "public_$action $*" _debuglog ">>> DONE public_$action $CM_fqdn $*" else _we "No valid action was specified" show_help fi echo _testStaging # ----------------------------------------------------------------------