Hamish Burke | 2025-06-29
Automating Certificate Renewal
Things to learn
- CertCentral API: auth, endpoints, formats
- Cert formats:
.pfx
vs.pem
+ usage - Service reload commands (
systemctl
, Windows bindings) - Hands-on:
openssl x509 -in cert.pem -noout -text
→ read validity, SANs, issuer - TLS Certificates
- Renewing with DigiCert
- Handling Key Scenarios
- e
acme.sh
for automatic certificate issuance and renewal:- Bash-only, cross-platform (Linux, macOS, Git Bash on Windows).
- Supports DNS-01 for wildcard/SAN certs.
- Allows
--reloadcmd
to restart services post-renewal. - Error handling via log-parsing email scripts (Linux & PowerShell).
- Scheduled renewal: (daily)
- Linux: via
crontab
. - Windows: via
schtasks
and PowerShell script.
- Linux: via
- Install certs with
--install-cert
to store them in proper locations and auto-reload services. - OpenSSL to generate self-signed certs or act as your own CA.
- Use rysync or scp to copy certs to LB, or run script on each individually (for load balancers)
- Monitor certs centrally:
acme.sh --list
- Prometheus, Zabbix/Nagios, or custom audit scripts.
Overall flow on how to automate renewals
- Install
acme.sh
on server- Acme.sh is entirely written in bash, so need to use gitbash on windows
curl https://get.acme.sh | sh -s email=my@example.com
wget -O - https://get.acme.sh | sh -s email=my@example.com
- Run --install-cert command for each domain on server, so point files to right place, and to get reloaded after renew
- On linux,
acme.sh --install
installs the crontab to run every day- We server fullchain.cer so clients get al intermediates
- On windows
- create a
.ps1
powershell script to run the process - use
schtasks
to schedule it daily in Task Scheduler
- create a
Below scripts run renewals, then email on error:
Scripts
Linux
#!/bin/bash
LOGFILE="$HOME/.acme.sh/renewal.log"
EMAIL="your_email@example.com"
SUBJECT="acme.sh renewal error on $(hostname) - $(date '+%Y-%m-%d %H:%M:%S')"
BODY="Your acme.sh renewal encountered an error. See attached log."
# Run acme.sh cron and capture output
~/.acme.sh/acme.sh --cron > "$LOGFILE" 2>&1
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ] || grep -iqE "error|fail" "$LOGFILE"; then
echo "$BODY" | mail -s "$SUBJECT" -a "$LOGFILE" "$EMAIL"
fi
crontab -e # open editor
0 9 * * * /path/to/your/acme-renewal-email.sh # schedule 9am everyday
Windows
$logFile = "$env:USERPROFILE\.acme.sh\renewal.log"
$emailTo = "your_email@example.com"
$emailFrom = "your_email@example.com"
$smtpServer = "smtp.gmail.com"
$smtpPort = 587
$subject = "acme.sh renewal error on $env:COMPUTERNAME - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
$body = "Your acme.sh renewal encountered an error. See attached log."
$user = "your_email@example.com"
$securePass = ConvertTo-SecureString "your_app_password_here" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential ($user, $securePass)
# Run acme.sh cron and capture output
$bash = "C:\Program Files\Git\bin\bash.exe"
$cmd = "~/.acme.sh/acme.sh --cron >> $logFile 2>&1"
$process = Start-Process -FilePath $bash -ArgumentList "-c `"$cmd`"" -NoNewWindow -Wait -PassThru
# Read log content
$logContent = Get-Content $logFile -Raw
# Check for errors in log or non-zero exit code
if ($process.ExitCode -ne 0 -or $logContent -match "(?i)error|fail") {
Send-MailMessage -From $emailFrom -To $emailTo -Subject $subject -Body $body -Attachments $logFile `
-SmtpServer $smtpServer -Port $smtpPort -UseSsl -Credential $cred
}
schtasks /create /tn "ACME_Renew_Email" /tr "powershell.exe -File \"C:\Path\To\run_acme_with_email.ps1\"" /sc daily /st 09:00 /rl highest /f
# check it exists
schtasks /query /tn "ACME_Renew_Email"
# delte it
schtasks /delete /tn "ACME_Renew_Email" /f
Handling Key Scenarios
Key-rotation?
- Though acme.sh allows for reuse of private keys, i'd recommend rotating them on each renewal. To prevent compromise
Using acme.sh with Digicert
As shown here, need the directory URL and Account Binding Credentials (KID,HMAC Key)
- Register account in acme.sh
# Only need to do once
acme.sh --register-account \
--server "https://one.digicert.com/mpki/api/v1/acme/v2/directory" \
--eab-kid "YOUR_KID" \
--eab-hmac-key "YOUR_HMAC_KEY" \
-m you@yourdomain.com
- Include server in
--install-cert
command
acme.sh --install-cert -d example.com \
--server "https://one.digicert.com/mpki/api/v1/acme/v2/directory" \
--key-file /etc/ssl/private/example.key \
--fullchain-file /etc/ssl/certs/example.pem \
--reloadcmd "systemctl reload nginx"
How to handle wildcard/SAN certificates
Challenge Plugins
-
dns_cf
(cloudflare) -
dns_aws
for Route 53 -
Use
DNS-01
challenge for any wildcard domain, asHTTP-01
won't work
# with Cloudflare plugin
export CF_Token="<your_token>"
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'
- For SAN-only certs, list each domain individually
acme.sh --issue -d app.example.com -d api.example.com -d www.example.com
What if renewal fails before expiry
- Crontab will retry daily until expiry
- Early renewal: set
acme.sh --set-defaut-renew-days 30
- Use script to send email on error
- Fallback plan: keep self-sign or backup cert to avoid downtime
- Manual debug:
acme.sh --renew -d example.com --debug
Where to audit certs status centrally?
acme.sh --list
dumps all certs and expiry dates- Monitoring tools
- Prometheus + blackbox_exporter
- Nagios/Zabbix SSL expiry checks
- CA Dashboards
- DigiCert CertCentral portal for corporate certs
- Custom scripts
- runs
openssl x509 -enddate
on each host and logs to central DB
- runs
OCSP Staping
- Enable on servers
- Means the client can make one less request to a OCSP responder
- Checks a key pair hasn't been revoked
HSTS (HTTP Stict Transport Security)
- Enforces HTTPS for websites
- And prevents users from bypassing security warnings for invalid/expired certificates
- Edit
nginx.conf
- In apache config, add
Strict-Transport-Security
header tohttpd.conf
- Enable in cloudflare dashboard
For load balancers
-
If certificates are centralised
- Copy them to load balancer, and reload LB service
--reloadcmd "rsync -avz /etc/ssl/certs/*.cert lb1:/etc/ssl/certs && ssh lb1 'systemctl reload nginx'"
-
If distributed
- Run
acme.sh
directly on each load balancer
- Run
-
Might require conversion between .pem and .pfx
- acme.sh gives you
.key
+.cer
(PEM format) - LB's sometimes between
.pfx
bundles openssl pkcs12 -export -out cert.pfx -inkey domain.key -in domain.cer -certfile ca.cer
To convert
- acme.sh gives you