-
-
Save greenmoss/8ee9d4acd3a21df699cde2225a78399e to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash | |
# This script renews letsecnrypt SSL certificates using Cloudflare dns-1 renewal | |
# It assumes you are using Mailcow | |
set -euo pipefail | |
# REQUIRED set these: | |
[email protected] | |
your_domain=mail.your.domain # only tested with single domain | |
cloudflare_ini_path=/root/.cloudflare # add your Cloudflare file here, called cloudflare.ini | |
# OPTIONAL also set these: | |
log_file=/var/log/certbot-cloudflare.log # if you don't want any logs, change it to /dev/null | |
# send all output and errors to log file | |
exec 1>$log_file | |
exec 2>&1 | |
# log what we are doing | |
set -x | |
date # overwrite, no log rotate! | |
echo "starting renewal" | |
docker pull certbot/dns-cloudflare | |
docker run --rm \ | |
-v $cloudflare_ini_path/cloudflare.ini:/cloudflare.ini \ | |
-v /opt/mailcow-dockerized/data/assets/ssl:/etc/letsencrypt \ | |
certbot/dns-cloudflare \ | |
certonly -n --agree-tos -m $your_email \ | |
--dns-cloudflare --dns-cloudflare-credentials /cloudflare.ini \ | |
-d $your_domain | |
cd /opt/mailcow-dockerized/data/assets/ssl | |
newcerts=$(find live/$your_domain/ -mmin -5) | |
if [ -z "$newcerts" ]; then | |
echo "no renewals found, not restarting services" | |
exit | |
fi | |
ln -sfv live/$your_domain/privkey.pem key.pem | |
ln -sfv live/$your_domain/cert.pem cert.pem | |
cd ../../.. | |
function reload_ssl_service () { | |
service=$1 | |
port=$2 | |
echo "restarting SSL service $1 on port $2" | |
docker-compose restart $service | |
timeout 30 sh -c '\ | |
while ! \ | |
openssl s_client -showcerts -connect $0:$1 2>/dev/null </dev/null | openssl x509 -noout 2>/dev/null; do | |
# $0 and $1 are inside single quotes, which means they expand to the arguments provided to sh -c | |
sleep 1 | |
done' $your_domain $port | |
echo "$service SSL cert expiration" | |
openssl s_client -showcerts -connect $your_domain:$port 2>/dev/null </dev/null | openssl x509 -noout -text | grep 'Not After' | |
} | |
reload_ssl_service nginx-mailcow 443 | |
reload_ssl_service dovecot-mailcow 993 | |
reload_ssl_service postfix-mailcow 465 | |
date | |
echo "completed" |
I fixed the too many arguments
problem, and got rid of -it
, which was preventing this script from running from cron.
I changed line 25 to the following to prevent hanging instances
docker run --rm \
I changed line 25 to the following to prevent hanging instances
docker run --rm \
Thanks, updated above!
Thanks for creating this.
Line 50, what should the $0 and $1 be expanding to? Isn't it supposed to be FQDN:PORT - ie mail.domain.com:443
The way its written it expands to the script name and docker service (ie ./certbot-dns-mailcow:nginx-mailcow )
Thanks!
Thanks for creating this.
Line 50, what should the $0 and $1 be expanding to? Isn't it supposed to be FQDN:PORT - ie mail.domain.com:443
The way its written it expands to the script name and docker service (ie ./certbot-dns-mailcow:nginx-mailcow )
Thanks!
$0
and $1
are inside single quotes, which means they expand to the arguments provided to sh -c
. You can test it this way:
timeout 30 sh -c 'echo $0 $1 > /tmp/sh.out' foo 123
cat /tmp/sh.out
This should show foo 123
Note: this script was a quick one-off, so I did this in an quick and dirty way. If I were going to write something more maintainable, I'd probably expand things out instead of using this big line full of shell meta-characters.
EDIT I added some newlines and a comment to the line, hopefully that makes it clearer. Thanks for your comment.
^^Thank you for explaining. Not a bash expert by any means but do like to understand.
So effectively the sh -c is starting a new shell and those 2 parameters at the end are getting passed as $0 and $1. What's confusing was/is that from what i've read $0 refers to the shell or file. In fact from the sh man,
it refers to the command name. In practice, it does get expanded as you stated. However, it appears the key is the single quotes, in which case the $0, $1 refers to the the literal first and second passed variables :).. Lots to learn
Btw, what is the purpose of that entire loop? Wait for up to 30 seconds until the service reports a valid cert? But it continues regardless after 30 sec?
^^Thank you for explaining. Not a bash expert by any means but do like to understand.
So effectively the sh -c is starting a new shell and those 2 parameters at the end are getting passed as $0 and $1. What's confusing was/is that from what i've read $0 refers to the shell or file. In fact from the sh man,
it refers to the command name. In practice, it does get expanded as you stated. However, it appears the key is the single quotes, in which case the $0, $1 refers to the the literal first and second passed variables :).. Lots to learn
Yeah, I'm not an expert either. I hack on it until it does what I want 😆
Btw, what is the purpose of that entire loop? Wait for up to 30 seconds until the service reports a valid cert? But it continues regardless after 30 sec?
Yup, that's right. I can't remember why I added that complexity, TBH. Maybe I was running it manually at first, wanted to verify the cert was actually renewed, and also didn't want to wait longer than necessary. LOL!
I'm seeing a strange issue.
After running the script to do a forced renewal, I'm seeing different expiry dates. This is confirmed by the ./helper-scripts/expiry-dates.sh script.
It appears only nginx has the updated cert, dovecot and postfix still using the old.
Have you seen this as well?
Perhaps my problem is/was the starting point. In the beginning I initially used the mailcow builtin acme client to do pull the initial certs while temporarily opening up ports 80/443 inbound.
I'm seeing a strange issue.
After running the script to do a forced renewal, I'm seeing different expiry dates. This is confirmed by the ./helper-scripts/expiry-dates.sh script.
It appears only nginx has the updated cert, dovecot and postfix still using the old.
Have you seen this as well?
Perhaps my problem is/was the starting point. In the beginning I initially used the mailcow builtin acme client to do pull the initial certs while temporarily opening up ports 80/443 inbound.
My first guess would be that the services didn't actually restart.
Logs indicate all 3 have successfully restarted. Restarting manually didn't make any difference.
I think the issue is path related. Mailcow docs say not to use simlinks for certs.
Built in acme client places certs in to /data/assets/ssl/{domain.com}. It appears /data/assets/ssl is then mapped to the containers as /etc/ssl/mail. Your script symlinks them to /data/assets/ssl directly. From there it gets confusing.
Perhaps there's been some changes to cert placement in the current version. (2023-05a).
Edit. Rereading the mailcow docs - specifically https://docs.mailcow.email/post_installation/firststeps-ssl/#how-to-use-your-own-certificate
Does indeed indicate
To use your own certificates, just save the combined certificate (containing the certificate and intermediate CA/CA if any) to data/assets/ssl/cert.pem and the corresponding key to data/assets/ssl/key.pem.
IMPORTANT: Do not use symbolic links! Make sure you copy the certificates and do not link them to data/assets/ssl.
I'm not sure how /data/assets/ssl/{domain.com} got created in my instance to begin with. It does have a date of few weeks ago, so perhaps it was made by the built in acme client. So the only change then is copying files rather than symlinking?
That works thanks!
I received the follow error but after the extra command below, this is now functioning correctly without error.
./certbot-dns-mailcow: line 29: [: too many arguments
The extra command
cp /opt/mailcow-dockerized/data/assets/ssl/live/domain.com/fullchain.pem /opt/mailcow-dockerized/data/assets/ssl/cert.pem