Hamish Burke | 2025-06-29


Automating Certificate Renewal

Things to learn

Overall flow on how to automate renewals

  1. Install acme.sh on server
    1. Acme.sh is entirely written in bash, so need to use gitbash on windows
    2. curl https://get.acme.sh | sh -s email=my@example.com
    3. wget -O - https://get.acme.sh | sh -s email=my@example.com
  2. Run --install-cert command for each domain on server, so point files to right place, and to get reloaded after renew
  3. On linux, acme.sh --install installs the crontab to run every day
    1. We server fullchain.cer so clients get al intermediates
  4. On windows
    1. create a .ps1 powershell script to run the process
    2. use schtasks to schedule it daily in Task Scheduler

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?

As shown here, need the directory URL and Account Binding Credentials (KID,HMAC Key)

  1. 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
  1. 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

# with Cloudflare plugin
export CF_Token="<your_token>"
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'
acme.sh --issue -d app.example.com -d api.example.com -d www.example.com

What if renewal fails before expiry

acme.sh --renew -d example.com --debug

Where to audit certs status centrally?

OCSP Staping

HSTS (HTTP Stict Transport Security)

For load balancers

Why not DigiBot/DigiCert?