86°F Knoxville
Mon–Fri 9–5 ETAnswered by a human
Infrastructure · IMPLEMENTED

Automating Wildcard SSL Certificates on Synology NAS with acme.sh

Deployed automated wildcard SSL certificate management for Synology NAS using acme.sh, Let's Encrypt, and Cloudflare DNS validation.

Summary

Deployed automated wildcard SSL certificate management for a Synology NAS using acme.sh, Let's Encrypt, and Cloudflare DNS validation. Certificates land in a custom directory structure so they survive DSM updates and backups, and they auto-renew and redeploy to DSM services without anyone touching DSM again.

The challenge

Synology's built-in Let's Encrypt support is fine for a single externally-reachable hostname, but it falls apart the moment you want anything more interesting. A short list of what it won't do:

  • no native wildcard support — you can't issue *.domain.com
  • it uses HTTP-01 validation, which means port 80 has to be open to the internet
  • internal and private subdomains that aren't publicly reachable aren't supported at all
  • no DNS-01 validation, so behind-firewall systems are stuck

We manage infrastructure with a stack of internal subdomains that shouldn't be publicly accessible — the DSM UI, internal dashboards, a reverse proxy for a handful of internal services. Cutting a cert per name through DSM meant either exposing everything on 80/443 or rolling our own, so we rolled our own.

Solution architecture

acme.sh is a pure-shell ACME client that supports DNS-01 validation against 50+ providers. Paired with Cloudflare's DNS API and acme.sh's built-in Synology deployment hook, we get fully automated issuance, renewal, and deployment with zero exposed ports.

  • acme.sh: lightweight, no Python or Go runtime required
  • Let's Encrypt: free CA, 90-day certs
  • Cloudflare DNS API: scripts the TXT record for the ACME challenge
  • synology_dsm hook: deploys the renewed cert into DSM and restarts affected services

Step 0: install acme.sh in a custom location

By default acme.sh installs into /root/.acme.sh, which is fine until a DSM update nukes it. We install into a sysadmin home directory so it persists, backs up cleanly, and is easy to find later.

code
# Create custom directories
mkdir -p /var/services/homes/sysadmin/acme.sh/acme_certs
mkdir -p /var/services/homes/sysadmin/acme.sh/acme_config

# Install acme.sh with custom paths
curl https://get.acme.sh | sh -s [email protected] \
  --home /var/services/homes/sysadmin/acme.sh/acme_config \
  --config-home /var/services/homes/sysadmin/acme.sh/acme_config \
  --cert-home /var/services/homes/sysadmin/acme.sh/acme_certs

Flags:

  • --home: where the acme.sh scripts live
  • --config-home: account config and logs
  • --cert-home: issued certificates
  • email: registers the account with Let's Encrypt for expiry notifications

Step 1: test in staging first

Let's Encrypt rate-limits production to 5 certs per domain per week. If you're troubleshooting DNS validation against a fresh zone, it is absurdly easy to burn through that. Always run against staging first, confirm the whole pipeline works, then flip to prod.

code
CF_Token="YOUR_CLOUDFLARE_API_TOKEN" \
CF_Email="[email protected]" \
SYNO_USE_TEMP_ADMIN=1 \
/var/services/homes/sysadmin/acme.sh/acme_config/acme.sh \
  --home /var/services/homes/sysadmin/acme.sh/acme_config \
  --cert-home /var/services/homes/sysadmin/acme.sh/acme_certs \
  --config-home /var/services/homes/sysadmin/acme.sh/acme_config \
  --staging \
  --issue \
  -d '*.internal.example.com' \
  --dns dns_cf

What those environment variables and flags do:

  • --staging: hit the Let's Encrypt staging endpoint (untrusted certs, no rate limit)
  • --dns dns_cf: use the Cloudflare DNS plugin for the challenge
  • CF_Token / CF_Email: Cloudflare credentials the plugin uses
  • SYNO_USE_TEMP_ADMIN=1: tells the Synology hook to create and tear down a temp admin for deployment — this is how you get around 2FA on the sysadmin account
  • -d '*.internal.example.com': wildcard name to issue

Under the hood, acme.sh publishes a TXT record at _acme-challenge.internal.example.com via the Cloudflare API, Let's Encrypt queries that record, and issues the cert. Nothing ever touches port 80.

Step 2: issue the production certificate

Once staging works, drop the --staging flag and point --server at the prod endpoint.

code
CF_Token="YOUR_CLOUDFLARE_API_TOKEN" \
CF_Email="[email protected]" \
SYNO_USE_TEMP_ADMIN=1 \
/var/services/homes/sysadmin/acme.sh/acme_config/acme.sh \
  --home /var/services/homes/sysadmin/acme.sh/acme_config \
  --cert-home /var/services/homes/sysadmin/acme.sh/acme_certs \
  --config-home /var/services/homes/sysadmin/acme.sh/acme_config \
  --server https://acme-v02.api.letsencrypt.org/directory \
  --issue \
  -d '*.internal.example.com' \
  --dns dns_cf

API endpoints for reference

STEP 3: AUTOMATED RENEWAL + DEPLOYMENT Renewal and deployment get wrapped into a single command run by Synology Task Scheduler. acme.sh only actually renews when the cert has less than 30 days left, so running this daily is a no-op most of the time — and on the day it matters, it just works.

code
# Combined Renew + Deploy (add to Task Scheduler)
CF_Token="YOUR_CLOUDFLARE_API_TOKEN" \
CF_Email="[email protected]" \
SYNO_USE_TEMP_ADMIN=1 \
/var/services/homes/sysadmin/acme.sh/acme_config/acme.sh \
  --home /var/services/homes/sysadmin/acme.sh/acme_config \
  --cert-home /var/services/homes/sysadmin/acme.sh/acme_certs \
  --config-home /var/services/homes/sysadmin/acme.sh/acme_config \
  --renew \
  --server https://acme-v02.api.letsencrypt.org/directory \
  --deploy \
  --deploy-hook synology_dsm \
  -d '*.internal.example.com'
  • --renew: only renews if the cert is close to expiry, exits cleanly otherwise
  • --deploy --deploy-hook synology_dsm: after renewal, install the new cert into DSM and restart affected services

In Task Scheduler: Control Panel > Task Scheduler > Create > Scheduled Task > User-defined script. User has to be root for the DSM deployment to work. Daily at 3:00 AM is fine.

Security notes

Create a scoped Cloudflare API token, not a Global API Key — permission set to Zone > DNS > Edit, scoped to the one zone you care about. The token is stored in acme_config/account.conf with permissions auto-set to 600; back that directory up somewhere safe.

SYNO_USE_TEMP_ADMIN spins up a temp admin user only for the 2-3 seconds needed to deploy the cert, then removes it. It's there because Synology's cert deployment requires admin privileges and there is no clean way around 2FA otherwise.

Outcome

Wildcard SSL for internal infrastructure, fully automated. One cert covers every subdomain. No ports exposed. Renewals happen at 3 AM, deploy themselves into DSM, and restart services without anyone noticing. The last manual cert touch on this NAS was the day we set this up.

  • single wildcard cert covers all internal subdomains
  • DNS-01 works regardless of firewall posture
  • renewals are scheduled, deployments are automatic
  • custom directory structure survives DSM updates and is trivial to back up

Key takeaways:

  • Synology's built-in Let's Encrypt is a toy — fine for one hostname, useless for anything else
  • DNS-01 is the right answer for internal infrastructure
  • always run staging first, especially against a new zone
  • custom install directories stop DSM updates from eating your cert manager

Tools & references