One weekly renewal batch. Several certificate consumers. One deliberate WebStation restart.
Over the course of fighting comatose printers, restarting WebStations, and generally dealing with appliances that refuse to renew certificates reliably, I have worked my way toward a comprehensive and complete certificate chain.
Somewhere along the way, I ended up completely tearing Synology's Web API apart and writing my own calls to make it do what I actually wanted in the first place.
I have a habit of fixing one automation problem and accidentally discovering the next three.
Ugly_Reload began because four forced
certificate renewals could finish faster than one Synology WebStation restart.
That created a race in which later certificate deployments could arrive while an earlier restart was still in progress.
Ugly_Reload removed that race. The first certificates could be installed without restarting WebStation, and the final certificate could perform one deliberate restart after the complete batch was already in place.
Problem solved.
Except Nginx Proxy Manager, Pi-hole, Plex and the router still wanted the same certificate material in different formats, in different directories and on different machines.
Apparently renewing certificates is the easy part.
Convincing the household that renewal happened requires administration.
certificate-chain.sh owns one complete weekly certificate run on Synology DSM.
It force-renews every configured wildcard certificate through acme.sh, backs
up the resulting certificate directories and deploys the same known source
material to every consumer.
The primary wildcard is used for Pi-hole, Plex and the router handoff. Every configured wildcard is copied to its matching Nginx Proxy Manager certificate ID.
NPM receives both the files and the correct expiry metadata. Pi-hole receives a
combined PEM file. Plex receives a PKCS#12 bundle using the password already
stored in Plex Preferences. The router receives three stable files through the
shared Incoming directory.
Nothing is sourced from NPM merely because NPM happens to have a copy.
Consumers are consumers. They are not promoted to certificate authorities just because the files are nearby.
The normal all action performs the production sequence in this order:
- Force-renew every configured wildcard certificate in configuration order.
- Back up every renewed ACME certificate directory.
- Deploy all certificates to Nginx Proxy Manager.
- Deploy the primary certificate to Pi-hole.
- Stage the primary certificate for the router.
- Build and deploy the primary certificate as PKCS#12 for Plex.
- Remove tightly named script-created history older than the configured retention period.
Independent deployment targets all get their chance. An NPM failure is still a failure, but it does not prevent Pi-hole, the router handoff or Plex from receiving their own certificate material.
Renewal and backup remain prerequisites. If the source material is not known to be complete, deployment does not begin.
The forced renewal policy is intentional.
I want every consumer to receive certificate material produced by the same controlled maintenance run. A mixture of newly renewed files and older but still valid files would probably work, but "probably" is not a particularly comforting deployment strategy at 04:45.
The persisted acme.sh configuration for each certificate decides how DSM is
updated during renewal.
In my batch, the early certificates use Ugly_Reload and therefore replace their
DSM certificates with restart_httpd=false. The final certificate retains the
normal synology_dsm deploy hook and performs the one deliberate WebStation
restart after the preceding certificates are already installed.
The order in CERTIFICATES therefore matters.
certificate-chain does not replace Ugly_Reload, and Ugly_Reload does not
replace certificate-chain. One prevents the DSM deployment race. The other
coordinates renewal, backup and every downstream consumer around that result.
One removes the race condition.
The other stops the remaining infrastructure from improvising.
| File | Runs on | Purpose |
|---|---|---|
certificate-chain.sh |
Synology DSM NAS | Renew, back up, deploy, test and prune the complete certificate batch |
certificate-chain.cfg.example |
Configuration example | Documents every installation-specific value used by the NAS script |
router-certificate-import.sh |
Synology SRM router | Validate and import the staged certificate, archive it and reboot the mesh safely |
mesh-health.sh |
Synology SRM router | Report post-maintenance mesh topology, health and uptime to Log Center |
nas-boot.sh |
Synology DSM NAS | Restore the TUN device and router certificate share after a NAS reboot |
The router scripts do not need a copy of certificate-chain.cfg. They consume
the three files placed in Incoming and discover the current mesh through SRM's
own API.
Copy the example beside the main script:
cp certificate-chain.cfg.example certificate-chain.cfgThen edit certificate-chain.cfg for the local installation.
The file is sourced as a trusted POSIX shell fragment. Keep it to assignments, quote values containing spaces and preserve the multiline certificate table.
| Setting | Purpose |
|---|---|
ACME_HOME |
Existing acme.sh home containing the certificate directories and renewal recipes |
ACME_SERVER |
Production ACME server name |
ACME_STAGING_SERVER |
Staging ACME server used by the isolated issuance test |
BACKUP_DIR |
Destination for certificate archives and isolated deployment tests |
RETENTION_DAYS |
Maximum age of tightly named script-created history |
NPM_SSL |
Nginx Proxy Manager custom certificate root |
NPM_DB |
Nginx Proxy Manager SQLite database |
NPM_CONTAINER |
Container restarted after a successful NPM deployment |
PIHOLE_DIR |
Pi-hole certificate directory |
PIHOLE_CONTAINER |
Container restarted after a successful Pi-hole deployment |
ROUTER_DST |
Mounted router Incoming directory |
PLEX_PACKAGE |
Synology Plex package name |
PLEX_DIR |
Local Plex PKCS#12 output and history directory |
PRIMARY_DOMAIN |
Wildcard shared by Pi-hole, Plex and the router |
CERTIFICATES |
Ordered domain:NPM-ID records for the complete renewal batch |
The public example uses reserved .example names. The live
certificate-chain.cfg belongs beside the script on the NAS and should not be
committed merely because Git is excellent at remembering regrettable things.
The main script stays close to POSIX shell, but its job is inherently platform-aware.
| Component | Expected environment |
|---|---|
| Shell | POSIX-style /bin/sh |
| Certificate client | Existing acme.sh installation and renewal context |
| NAS tools | tar, find, OpenSSL, Python 3, sqlite3 and the Docker CLI |
| DSM service control | /usr/syno/bin/synosystemctl for Plex |
| NPM | Direct access to its custom SSL directories and SQLite database |
| Pi-hole | Direct access to its TLS directory and container |
| Plex | Read access to Preferences.xml and write access to the PKCS#12 location |
| Router handoff | Mounted Synology router Certificates/Incoming directory |
The script requires root because certificate locations, container control and Synology service control are privileged operations. This is not a philosophical position on user management. It is merely Linux declining to negotiate.
After creating the live configuration, perform the checks in this order:
- Confirm that the configuration loads and inspect the available actions.
sudo ./certificate-chain.sh help- Exercise every deployment against an isolated directory tree and a copied NPM database.
sudo ./certificate-chain.sh test- Exercise renewal against Let's Encrypt staging without consuming production issuance capacity.
sudo ./certificate-chain.sh staging- Run the real chain only when the configuration and isolated tests are satisfactory.
sudo ./certificate-chain.sh allThe staging action clones the existing ACME context under /tmp, removes
persisted deploy, install and command hooks from the clone, renews against the
staging CA and feeds the untrusted results through the isolated deployment
test.
Staging certificates never enter production consumers.
That would be a surprisingly elaborate way to make every browser angry.
| Action | Behaviour |
|---|---|
renew |
Force-renew every configured wildcard certificate |
backup |
Archive every configured ACME certificate directory |
npm |
Deploy all certificates to NPM and repair its expiry metadata |
pihole |
Deploy the primary certificate to Pi-hole |
plex |
Build and deploy the primary Plex PKCS#12 bundle |
router |
Copy the primary leaf, key and CA to router Incoming |
deploy |
Run NPM, Pi-hole, router and Plex deployment |
prune |
Remove tightly named script-created history beyond retention |
all |
Run production renewal, backup, deployment and pruning |
test |
Test all deployment functions without changing production targets |
staging |
Test staging issuance, backup and deployment in isolation |
help |
Show the current interface and loaded configuration path |
Several actions may be supplied in one invocation. They are processed from left to right, and the final exit status remembers if one failed.
NPM stores certificate files under fixed certificate-ID directories, but copying those files does not update the expiry date in its database.
The NPM deployment therefore copies every certificate pair through temporary names, backs up the SQLite database, derives the real expiry date from each certificate, updates the corresponding record and restarts the container only after the complete set is ready.
The metadata repair is not cosmetic. An interface confidently displaying the wrong expiry date is still wrong, even when it uses a pleasant colour scheme.
Plex expects PKCS#12 rather than loose PEM files.
The script reads customCertificateKey from Plex's existing Preferences.xml,
passes it to OpenSSL through a file descriptor and never stores the password in
the script or process argument list.
The previous PKCS#12 bundle is copied to old/ before replacement. Plex is then
restarted through the Synology package service.
NPM is not involved in this path. Plex receives its certificate directly from the same ACME source as every other consumer.
Family trees become much easier to reason about when cousins stop pretending to be parents.
certificate-chain.sh places exactly three stable files in Incoming:
cert.pem
privkey.pem
chain.pem
router-certificate-import.sh validates that all three exist, parses the leaf
certificate and proves that the certificate public key matches the private key.
It then backs up the active router certificate, installs the new files, restarts
the router web service and moves the consumed input into a timestamped
Imported directory.
Before changing the certificate, it asks SYNO.Mesh.Node.List for the live mesh
topology and proves that every child has a complete path to node 0.
The reboot list is child-first. Wireless nodes are selected before eligible indirect ethernet nodes, which are selected before ethernet nodes attached directly to node 0. Each reboot signal is sent separately with a two-second pause. The main router is rebooted only after every mesh-node command has been accepted.
The order is based on the mesh that exists now, not a diagram someone happened to see in SRM two weeks ago.
Use test mode to display the exact current order without importing a certificate, restarting a service or sending a reboot command:
sudo /bin/sh /usr/syno/local/router-certificate-import.sh testThis is the safest full-mesh reboot order I can derive from the API SRM actually provides.
It is still a full-mesh reboot.
Plan accordingly.
mesh-health.sh is intended to run after the maintenance window has given the
network time to return.
It combines synomeshctl uptime information with the live
SYNO.Mesh.Node.List response and writes one line per node directly to SRM's
local RFC 5424 syslog socket.
Healthy node lines deliberately use Warning level. Synology renders them yellow, which makes the weekly report visible inside the surrounding INFO spam.
The colour is a highlighter, not a diagnosis.
Each line records the node ID, name, uptime, network state, health state, parent, link type, client count and signal value.
The old SRM BusyBox shell does not provide command -v, so the script addresses
SRM's Python 2.7 installation directly as /usr/bin/python. Portability is a
lovely goal, but pretending a private router API is portable would be performance
art.
nas-boot.sh handles the prerequisites that may disappear during a NAS reboot.
It recreates /dev/net/tun when needed, loads tun.ko, verifies the real CIFS
mount for the router Certificates share and confirms that Incoming is
available before declaring success.
The mount receives six bounded attempts because a NAS reaching the login screen does not necessarily mean its network, the router and CIFS have finished their morning coffee.
The supplied script contains installation-specific Synology paths, an internal router address and an explicit SMB 1.0 compatibility choice. Review those values before using it elsewhere.
Scheduling is installation policy and is not hard-coded into the scripts.
In my environment the NAS runs the complete chain during the Friday maintenance window. The router imports the staged certificate shortly afterward, and the mesh health report runs only after the network has had time to recover.
nas-boot.sh is registered as a DSM boot-up task rather than a calendar job.
The useful ordering is:
- Run
certificate-chain.sh allon the NAS. - Run
router-certificate-import.shon the router after the NAS handoff is complete. - Run
mesh-health.shafter the planned router and mesh reboot window.
The exact clock times belong to the installation. The dependency order does not.
Let's Encrypt certificates have a short useful life, so ancient deployment history eventually becomes stale rather than reassuring.
The NAS keeps only the explicitly named archive, NPM database backup, Plex
PKCS#12 backup and failed-test formats created by the script. The router applies
the same principle to timestamped Backup and Imported directories.
Unexpected names are ignored. Cleanup is deliberately a narrow allowlist, not a
general invitation for rm -rf to express itself creatively.
This is working code from my own Synology DSM and SRM environment.
The shell is intentionally conservative because both systems ship BusyBox and private platform tools with their own opinions about modern convenience.
Read the scripts. Read the configuration example. Read the Ugly_Reload warning
about Le_DeployHook before adopting that part of the batch design.
No guarantees.
No magic.
Just certificates, controlled timing, backups, validation and mild stubbornness.
MIT.
Use it, change it and adapt it to your own environment.
If it becomes exciting during the maintenance window, the logs are probably a better first witness than biological memory.