Let's Encrypt-Zertifkat einer Sophos UTM automatisiert weiterverarbeiten (Bash Script)

Seit der Firmware 9.6 unterstützt die Sophos UTM das automatisierte Verwalten von Let's Encrypt-Zertifikaten, um diese z.B. im WAF aber auch anderen Bereichen zu verwenden.
Leider nur ACMEv1, also keine Wildcard-Zertifikate. Aber immerhin gehen SAN-Zertifikate mit ziemlich vielen Alternate Subject Names (bei mir aktuell 16).

Nun hat man das Zertifikat auf der UTM, aber wie bekommt man es auf andere Systeme, z.B. den Webserver oder das Mailsystem?

Dafür habe ich mir quick & dirty ein Bash-Script zusammengebaut, das ein bestehendes Zertifikat mit dem auf der UTM vergleicht und bei Abweichung das der UTM herunterlädt und das lokale ersetzt.
Anschließend startet es die beteiligten Dienste neu, damit diese auch das neue Zertifikat verwenden.

Vorarbeiten

UTM API

Auf der UTM muss die API aktiviert und ein API-Key angelegt sein.

Außerdem benötigt man die REF-ID des abzufragenden Zertifikats. Diese bekommt man am Leichtesten, indem man sich an der API mit einem Browser anmeldet, oben rechts den Bereich "ca" auswählt und dann im Bereich "ca/host_key_cert" den GET des "/objects/ca/host_key_cert/" per "Try it out!" ausführt.
Dann die REF-ID des zugehörigen Zertifikatnamens im "Response Body" suchen.

Zielsystem

Auf dem Linux-System müssen cURL, jq und die mailutils installiert sein, außerdem muss das System in der Lage sein, Mails zu versenden (z.B. Postfix als Satellite oder Nullmailer und Konsorten).

Des Weiteren muss das Intermediate-CA-Zertifikat von Let's Encrypt passend im System vorhanden sein und im Script angegeben werden, falls die verwendete Software nicht die explizite Verwendung eines solchen erlaubt. Dovecot z.B. kennt keine Möglichkeit, ein Intermediate CA separat anzugeben, es muss im Public Cert mit eingefügt werden. Glücklicherweise können auch die meisten anderen System damit umgehen (z.B. Apache), so dass das kein Problem ist.
Falls man nur Programme betreibt, die eine separate Option für Intermediate CAs erlauben kann man diese Möglichekit natürlich nutzen und im Script die Variable intermediateca=false setzen.

Script

/usr/local/bin/getcert_utm.sh

#!/bin/bash
# Fetch Let'sEncrypt cert from UTM and check if different to local.
# If yes, overwrite and reload affected services
# Needs curl, jq and mailutils installed

log="/usr/bin/logger -i -p local0.info -t getcert_utm.sh"
log_dbg="/usr/bin/logger -i -p local0.debug -t getcert_utm.sh"
log_err="/usr/bin/logger -i -p local0.warning -t getcert_utm.sh"
#debug=true
$log "Starting getcert_utm.sh"

utmapiurl="https://<myutm.mydomain.de>:4444/api/objects/ca/host_key_cert/<REF-ID of LE-cert>"
utmapikey="<your API key here>"
# do not use whitespaces or special characters in paths!
curloutfile="/tmp/utm-le-cert_all"
tmpkeyfile="/tmp/utm-le-cert.key"
tmpcertfile="/tmp/utm-le-cert.pem"
# must match the path and certificate name in your apache, dovecot, etc. config
dstkeyfile="/etc/ssl/private/mydomain.de_SAN_LE.key"
dstcertfile="/etc/ssl/mydomain.de_SAN_LE.pem"
# false (if can be used separately) or path to intermediate
intermediateca="/etc/ssl/lets-encrypt-x3-cross-signed.pem"
tmpmailtext="/tmp/mailtext.txt"
mailsubject="LE-Cert-Fetcher on "$(hostname)": "
mailto="<your email address>"
mailresult="Nothing changed"
restartservices=false
# services to restart after certificate change
services="dovecot.service postfix.service nginx.service"
errtext=""
errdetected=false

certsubject=$(/usr/bin/openssl x509 -text -noout -in $dstcertfile |grep -oP '(?<=Subject: )[^,]+')
certvalidity=$(/usr/bin/openssl x509 -text -noout -in $dstcertfile |grep -oP '(?<=Not After : )[^,]+')
echo -e "Hello!\n" > $tmpmailtext
echo -e "This is to inform you about recent changes in our Let's Encrypt certificate with $certsubject:\nValid until: $certvalidity\n" >> $tmpmailtext
$log "Downloading cert from UTM API."
curl -k -X GET --header "Accept: application/json" --header "Authorization: Basic ${utmapikey}" "${utmapiurl}" -o "$curloutfile" > /tmp/getcert_utm_curl 2>&1
if [ $? -eq 0 ]; then
  $log "Extracting key and certificate from output."
  jq -r '.key' $curloutfile > $tmpkeyfile
  res1=$?
  jq -r '.certificate' $curloutfile > $tmpcertfile
  res2=$?
  if [ $res1 -eq 0 ] && [ $res2 -eq 0 ]; then
    $log "Comparing new and old files."
    diff $tmpkeyfile $dstkeyfile
    if [ $? -eq 0 ]; then
      $log "Key has not changed."
      echo "Key has not changed." >> $tmpmailtext
    else
      $log "Key has changed. Copying new key."
      cp $tmpkeyfile $dstkeyfile
      if [ $? -eq 0 ]; then
        restartservices=true
        mailresult="Key changed"
        echo "Key has been exchanged by a newer version." >> $tmpmailtext
      else
        errdetected=true
        errtext=$errtext"Error copying key to destination. "
        mailresult="Error"
        echo "ERROR copying key to destination" >> $tmpmailtext
      fi
    fi
    if [ $intermediateca != false ]; then
      $log "Appending intermediate ca to certificate."
      cat $intermediateca >> $tmpcertfile
    fi
    diff $tmpcertfile $dstcertfile
    if [ $? -eq 0 ]; then
      $log "Certificate has not changed."
      echo "Certificate has not changed." >> $tmpmailtext
    else
      $log "Certificate has changed. Copying new key."
      cp $tmpcertfile $dstcertfile
      if [ $? -eq 0 ]; then
        restartservices=true
        mailresult="Certificate changed"
        echo "Certificate has been exchanged by a newer version." >> $tmpmailtext
      else
        errdetected=true
        errtext=$errtext"Error copying certificate to destination. "
        mailresult="Error"
        echo "ERROR copying certificate to destination" >> $tmpmailtext
      fi
    fi
    if [ "$errdetected" = true ]; then
      mailresult="Error"
      $log_err "Failure in setting certs: $errtext"
    fi
  else
    mailresult="Error extracting cert"
    echo "Could not extract cert or key from output. Check format of output, should be JSON containing .key and .certificate section." >> $tmpmailtext
    $log_err "Could not extract cert or key from output. Check format of output, should be JSON containing .key and .certificate section."
  fi
else
  mailresult="Error getting cert"
  echo "Could not connect to $utmapiurl or error in authentication. Check connection and URL settings." >> $tmpmailtext
  $log_err "Could not connect to $utmapiurl or error in authentication. Check connection and URL settings."
fi
certdetails=$(/usr/bin/openssl x509 -text -noout -in $dstcertfile)
echo -e "\nBest regards,\nroot\n\n----------\n\nCertificate details:\n\n$certdetails" >> $tmpmailtext
mail -s "${mailsubject}${mailresult}" $mailto < "${tmpmailtext}"
rm "${curloutfile}" "${tmpkeyfile}" "${tmpcertfile}" "${tmpmailtext}"
if [ "$restartservices" = true ]; then
  $log "Reloading \"$services\" as some data has changed"
  systemctl reload $services
fi
$log "Finished."

#Finished

Finally

Am Schluss muss man das Script natürlich noch irgendwie regelmäßig ausführen, z.B. mit einem täglichen Cron-Job, oder SystemD, oder...