Automating SSL cert renewal on Synology NAS, without exposing it to the World Wild Web

It sounds fairly specific, but you can abstract the takeaways.

In the past, I posted about my setup to protect my NAS making it accessible only via Tailscale.

My closing notes specifically regarding my SSL configuration were:

Not ideal, but if you ask me worth it compared to having to accept the self-signed alternative on every browser and machine.

A couple of weeks ago, I happened to be in my home town to visit my folks, and I discovered that BolognaJs was having a meetup, and I was lucky enough to hear Valerio talk about his homelab setup.

The video is in Italian, but the TL;DR is in the rest of the post

His setup was incredibly similar to mine, with the exception that he automated the SSL renewal and pointed his Cloudflare DNS to his internal IPs instead of the Tailscale ones (I'm going to elaborate later on why this is helpful).

The way I was dealing with the SSL renewal, as I assumed Let's Encrypt needed to access my machine to do so, was to temporarily open port 80 and 443 to my router, exposing it briefly to the internet (and also changing my Cloudflare A record to point from the Tailscale IP to my home public IP).

It was laborious and I often forgot to do it before the certificates' expiration, ending up having to do it at the most inconvenient times.

Thanks to Valerio, I discovered that my assumption was wrong – as they often are: an SSL certificate can be renewed with just a TXT record on the DNS, to prove ownership. And this can be fully automated with Cloudflare.

The script is somewhat specific to my setup, so consider it a starting point, rather than a recipe ready to go[^1].

#!/bin/bash
set -e

# ========================================
# CONFIGURATION - UPDATE THESE VALUES
# ========================================

# This file should be saved in the home folder of the user specified here
SYNOLOGY_USER="<INSERT-YOUR-USERNAME>"

# Cloudflare credentials
CF_TOKEN="<INSERT-YOUR-CLOUDFLARE-TOKEN>"
CF_EMAIL="<INSERT-YOUR-CLOUDFLARE-EMAIL>"

# Primary domain (will create wildcard certificate for *.yourdomain.com)
PRIMARY_DOMAIN="<INSERT-YOUR-FQDN>" # Top Level domain name
SYNO_DOMAIN="<THE-FQDN-OF-YOUR-NAS>" # a 2nd level domain name pointing to the NAS

# DSM settings (only needed if you changed default HTTPS port)
DSM_PORT="443" # Default is 5001, change if different

# acme.sh installation directory
ACME_HOME="/usr/local/share/acme.sh"

# Email for Let's Encrypt account
ACME_EMAIL="<INSERT-AN-EMAIL-FOR-LETSENCRYPT>"

# ========================================
# FUNCTIONS
# ========================================

install_acme() {
echo "Installing acme.sh..."
cd /var/services/homes/$SYNOLOGY_USER
wget -O master.tar.gz https://github.com/acmesh-official/acme.sh/archive/master.tar.gz
tar xvf master.tar.gz
cd acme.sh-master/
./acme.sh --install --nocron --home "$ACME_HOME" --accountemail "$ACME_EMAIL"

# Source profile to make acme.sh available
[ -f ~/.profile ] && source ~/.profile

echo "acme.sh installed successfully"
}

issue_wildcard_certificate() {
echo "Issuing wildcard certificate for *.${PRIMARY_DOMAIN}..."

export CF_Token="$CF_TOKEN"
export CF_Email="$CF_EMAIL"

cd "$ACME_HOME"
./acme.sh --issue --server letsencrypt --home "$ACME_HOME" -d "*.${PRIMARY_DOMAIN}" --dns dns_cf

echo "Wildcard certificate issued successfully"
echo "This certificate covers *.${PRIMARY_DOMAIN}"
}

deploy_to_dsm() {
echo "Deploying certificate to DSM..."

export SYNO_LOCAL_HOSTNAME=1
export SYNO_USE_TEMP_ADMIN=1
export SYNO_CREATE=1
export SYNO_HOSTNAME="$SYNO_DOMAIN"
export SYNO_SCHEME="https"
export SYNO_PORT="$DSM_PORT"

cd "$ACME_HOME"
./acme.sh --deploy --insecure --home "$ACME_HOME" -d "*.$PRIMARY_DOMAIN" --deploy-hook synology_dsm

echo "Certificate deployed to DSM successfully"
}

# ========================================
# MAIN SCRIPT
# ========================================

echo "=========================================="
echo "Synology Wildcard SSL Certificate Manager"
echo "=========================================="
echo ""
echo "Certificate Coverage:"
echo " - *.${PRIMARY_DOMAIN} (all subdomains)"
echo ""
echo "=========================================="
echo ""

# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "ERROR: This script must be run as root"
echo "Please run: sudo su"
exit 1
fi

# Check if acme.sh is installed
if [ ! -f "$ACME_HOME/acme.sh" ]; then
echo "acme.sh not found. Installing..."
install_acme
else
echo "acme.sh already installed at $ACME_HOME"
fi

# hack for glob working slighlty weirdly on the synology shell
shopt -s nullglob
eval 'CERT_DIRS=( "$ACME_HOME"/*."$PRIMARY_DOMAIN"_ecc )'
shopt -u nullglob

# Check if certificate already exists
if [ ${#CERT_DIRS[@]} -gt 0 ]; then
echo "Certificate for $PRIMARY_DOMAIN already exists. Renewing..."
cd "$ACME_HOME"
./acme.sh --renew --home "$ACME_HOME" -d "*.$PRIMARY_DOMAIN" --force
else
echo "No existing certificate found. Issuing new wildcard certificate..."
issue_wildcard_certificate
fi

# Deploy to DSM
deploy_to_dsm

echo ""
echo "=========================================="
echo "Certificate deployment completed!"
echo "=========================================="
echo ""

And I haven't really optimised the script in any way, so if it suck blame it on Perplexity

Create a Cloudflare token for your domain with DNS:Edit permissions; fill the variables at the top of the script; launch the script as root.

It should install acme.sh if not available, then use it to create or renew the SSL certificates and ultimately deploy them in Synology, making them available in the DSM control panel.

The next bit is automating the script execution with the NAS task scheduler

Make sure you run it with the root user.

There's a caveat: you need to disable MFA and adaptive MFA, but if you are not exposed to the internet, this should not compromise your NAS security.

Bonus point: Plex.

If you run Plex on your NAS, you probably know it requires its own certificates in a dedicated folder, and it only deals with a specific format. Add this to the execution path (you'll also need to add 2 env variables at the top: PLEX_CERT_PASSWORD and PLEX_CERT_DIR)

deploy_to_plex() {
echo "Deploying certificate to Plex Media Server..."

# Convert certificate to PKCS12 format for Plex
cd "$ACME_HOME"
./acme.sh --toPkcs --home "$ACME_HOME" -d "*.${PRIMARY_DOMAIN}" --password "$PLEX_CERT_PASSWORD"

if [ $? -eq 0 ]; then
echo "Certificate converted to PKCS12 format"

# Copy certificate to Plex directory
cp "${ACME_HOME}/*.${PRIMARY_DOMAIN}_ecc/*.${PRIMARY_DOMAIN}.pfx" "${PLEX_CERT_DIR}/plex_cert.pfx"

# Set proper ownership and permissions
chown PlexMediaServer:PlexMediaServer "${PLEX_CERT_DIR}/plex_cert.pfx"
chmod 644 "${PLEX_CERT_DIR}/plex_cert.pfx"

echo "Restarting Plex Media Server..."
/usr/syno/bin/synopkg stop PlexMediaServer
sleep 3
/usr/syno/bin/synopkg start PlexMediaServer

echo "Plex certificate deployed and service restarted successfully"
else
echo "ERROR: Certificate conversion failed"
exit 1
fi
}

Last bit of improvement: removing Tailscale IPs from Cloudflare A record.

This was a minor, yet noticeable, pain point: I have 2 networks at home and the NAS is deliberately only visible in one of them (forcing me to use Tailscale when on the other). When I switch networks, and I turn on Tailscale, more often than not browsers took a bit to update their DNS cache. On top of that, when I connect my machine to ExpressVPN, I can't really access the services anymore, as ExpressVPN sees the Cloudflare configuration, not the local one (please read the previous post to grasp better the context here).

Tailscale solves that for you.

tailscale set --advertise-routes=<YOUR-LOCAL-NETWORK-NAS-IP>/32

Running this command on your NAS (as root) would configure Tailscale to route the traffic of your local NAS IP, effectively mimicking a local network connection.

You'll have to enable it in the Tailscale admin panel too, but then it's done.

Clicking "Edit route settings..." on the NAS entry opens a panel with a checkbox near the adveritsed ip, enable it.

As a benefit, now I can connect to my local services when I'm on my network but connected to ExpressVPN: with the domain pointing to a local IP, it doesn't conflict with ExpressVPN routing.

[^1] for instance, I believe it won't work if you don't have a valid certificate to begin with, so it's good to automate something manual until then, but not as a first setup.