SSL: automatic renewal

Linux server — nginx, Apache, Caddy, HAProxy, Docker

A script for nginx, Apache, Caddy, HAProxy and Docker fetches the fresh certificate daily, checks it against your key and reloads the server with no downtime.

Renews by itself

Use this if you have SSH access to the server and root rights (a VPS, a dedicated server, your own machine in a rack). You need curl and openssl, which almost every Linux has.

What you need

  • The certificate link from your account: https://api.zrazu.com/ssl-pull/zsp_YOUR_TOKEN.
  • The private key the certificate was ordered with (a key.pem file or similar). If the CSR was created by the ZRAZU account, this is the file you saved back then.
  • 10 minutes.

Step 1. Put the key on the server

sudo mkdir -p /etc/ssl/zrazu/example.com
sudo chmod 700 /etc/ssl/zrazu/example.com
sudo nano /etc/ssl/zrazu/example.com/key.pem
sudo chmod 600 /etc/ssl/zrazu/example.com/key.pem

In nano, paste the whole key text, including the -----BEGIN … PRIVATE KEY----- and -----END … PRIVATE KEY----- lines, save (Ctrl+O, Enter) and exit (Ctrl+X). The key must have no password: if yours is password-protected, remove the password with openssl pkey -in old.key -out /etc/ssl/zrazu/example.com/key.pem.

Step 2. The renewal script

Create the file with sudo nano /usr/local/bin/zrazu-ssl-example.com.sh, paste the text below and change only the RELOAD line for your web server:

  • nginx — nginx -t && systemctl reload nginx (this is what is set);
  • Apache (Debian, Ubuntu) — apachectl configtest && systemctl reload apache2; Apache (CentOS, Rocky, Alma) — apachectl configtest && systemctl reload httpd;
  • Caddy — systemctl reload caddy;
  • HAProxy — systemctl reload haproxy and COMBINED=1 (HAProxy reads the certificate and key from one file, combined.pem);
  • nginx in Docker — docker exec CONTAINER_NAME nginx -s reload (mount the /etc/ssl/zrazu directory into the container as a volume).
#!/bin/sh
# ZRAZU: updates the SSL certificate using the personal link. Run by cron once a day.
set -eu
# cron gives a short PATH without /usr/sbin, and nginx and apachectl live there.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PULL="https://api.zrazu.com/ssl-pull/zsp_YOUR_TOKEN"
DIR="/etc/ssl/zrazu/example.com"
KEY="$DIR/key.pem"
RELOAD="nginx -t && systemctl reload nginx"
COMBINED=0

umask 077
tmp=$(mktemp "$DIR/.new.XXXXXX")
trap 'rm -f "$tmp"' EXIT
curl -fsS --retry 3 --max-time 60 -o "$tmp" "$PULL/fullchain.pem"
grep -q "BEGIN CERTIFICATE" "$tmp" || { echo "ZRAZU: the link does not return a certificate"; exit 1; }
if [ -f "$DIR/.applied" ] && cmp -s "$tmp" "$DIR/.applied"; then
  echo "ZRAZU: certificate unchanged"
  exit 0
fi
cert_pub=$(openssl x509 -in "$tmp" -noout -pubkey | openssl sha256)
key_pub=$(openssl pkey -in "$KEY" -pubout | openssl sha256)
if [ "$cert_pub" != "$key_pub" ]; then
  echo "ZRAZU: certificate was issued for a different key — changing nothing, put the new key in $KEY"
  exit 1
fi
openssl x509 -in "$tmp" -noout -checkend 0 >/dev/null || { echo "ZRAZU: certificate has already expired"; exit 1; }
chmod 644 "$tmp"
mv -f "$tmp" "$DIR/fullchain.pem"
if [ "$COMBINED" = 1 ]; then
  cat "$DIR/fullchain.pem" "$KEY" > "$DIR/combined.pem.new"
  mv -f "$DIR/combined.pem.new" "$DIR/combined.pem"
fi
sh -c "$RELOAD"
cp "$DIR/fullchain.pem" "$DIR/.applied"
echo "ZRAZU: new certificate installed, valid until $(openssl x509 -in "$DIR/fullchain.pem" -noout -enddate | cut -d= -f2)"

The script changes nothing if the web server already runs this certificate, if the file at the link is not a certificate, or if the certificate does not match your key or has already expired. It checks the web server configuration before reloading, so a broken config will not take your site down; and if the reload fails, the script tries again the next day.

Make the file executable and run it once by hand:

sudo chmod 700 /usr/local/bin/zrazu-ssl-example.com.sh
sudo /usr/local/bin/zrazu-ssl-example.com.sh

You should see "new certificate installed" and the file /etc/ssl/zrazu/example.com/fullchain.pem should appear.

Step 3. Point the web server at the files (once)

nginx — in the site's server block:

ssl_certificate     /etc/ssl/zrazu/example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/zrazu/example.com/key.pem;

Apache 2.4.8 and newer — in <VirtualHost *:443>:

SSLCertificateFile    /etc/ssl/zrazu/example.com/fullchain.pem
SSLCertificateKeyFile /etc/ssl/zrazu/example.com/key.pem

Caddy — in the site block: tls /etc/ssl/zrazu/example.com/fullchain.pem /etc/ssl/zrazu/example.com/key.pem. HAProxy — in bind :443 ssl crt /etc/ssl/zrazu/example.com/combined.pem.

Check the config (nginx -t or apachectl configtest) and reload the web server.

Step 4. Schedule

echo "17 4 * * * root /usr/local/bin/zrazu-ssl-example.com.sh >> /var/log/zrazu-ssl.log 2>&1" | sudo tee /etc/cron.d/zrazu-ssl-example.com

The script will run every day at 04:17 server time. You can pick any minute — just better not exactly 00.

Check

  1. sudo tail /var/log/zrazu-ssl.log — tomorrow it will show "certificate unchanged": the schedule works.
  2. The site serves our certificate: echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -issuer -enddate or the page https://zrazu.com/tools/ssl/example.com/.
  3. In your account, the certificate card shows that the server fetched the current certificate.

If it did not work

  • curl: (22) … 404 — the link is disabled or replaced. Create a new one in the certificate card and replace the PULL line in the script.
  • curl: (22) … 429 — too many requests (over 60 per hour). Once a day is well within the limit.
  • "certificate was issued for a different key" — the certificate was reissued with a new key. Put the new key in key.pem and run the script again.
  • nginx: [emerg] after the reload — an error in the site configuration, not in the certificate; the old certificate stays in use until you fix the error.

This page in Markdown