Skip to content

Instantly share code, notes, and snippets.

@greenmoss
Last active May 23, 2024 18:13
Show Gist options
  • Save greenmoss/8ee9d4acd3a21df699cde2225a78399e to your computer and use it in GitHub Desktop.
Save greenmoss/8ee9d4acd3a21df699cde2225a78399e to your computer and use it in GitHub Desktop.
This script renews letsecnrypt SSL certificates using Cloudflare dns-1 renewal. It assumes you are using Mailcow.
#!/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:
your_email=letsencrypt@your.domain
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"
@romprod
Copy link

romprod commented Jan 4, 2020

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

@greenmoss
Copy link
Author

I fixed the too many arguments problem, and got rid of -it, which was preventing this script from running from cron.

@romprod
Copy link

romprod commented Dec 6, 2020

I changed line 25 to the following to prevent hanging instances

docker run --rm \

@greenmoss
Copy link
Author

I changed line 25 to the following to prevent hanging instances

docker run --rm \

Thanks, updated above!

@gpz1100
Copy link

gpz1100 commented Jul 18, 2023

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!

@greenmoss
Copy link
Author

greenmoss commented Jul 18, 2023

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.

@gpz1100
Copy link

gpz1100 commented Jul 18, 2023

^^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,

image

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?

@greenmoss
Copy link
Author

^^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,

image

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!

@gpz1100
Copy link

gpz1100 commented Jul 27, 2023

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.

@greenmoss
Copy link
Author

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.

@gpz1100
Copy link

gpz1100 commented Jul 28, 2023

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?

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