SSL Certificate Expiration Notifications
A certificate that was supposed to be renewed automatically by certbot sometimes isn’t renewed: a hook failed, DNS changed, a token expired, or disk space ran out. Finding out about this 14 days in advance is fine; finding out 2 hours before is a catastrophe.
Check script
Section titled “Check script”/usr/local/bin/notifly-cert-check:
#!/usr/bin/env bash# Usage:# notifly-cert-check example.com [example.com:8443] [...]set -euset -a; source /etc/notifly.env; set +a
WARN_DAYS=14CRIT_DAYS=3
for HOSTPORT in "$@"; do HOST="${HOSTPORT%%:*}" PORT="${HOSTPORT##*:}" [ "$PORT" = "$HOST" ] && PORT=443
EXPIRE=$(echo | openssl s_client -servername "$HOST" -connect "$HOST:$PORT" \ -showcerts 2>/dev/null \ | openssl x509 -noout -enddate \ | cut -d= -f2)
if [ -z "$EXPIRE" ]; then /usr/local/bin/notifly-send \ "🚨 SSL: не смог проверить $HOSTPORT" \ "openssl не вернул дату. Возможно, сервер недоступен." 8 continue fi
EXP_TS=$(date -d "$EXPIRE" +%s) NOW_TS=$(date +%s) DAYS_LEFT=$(( (EXP_TS - NOW_TS) / 86400 ))
if [ "$DAYS_LEFT" -lt "$CRIT_DAYS" ]; then /usr/local/bin/notifly-send \ "🔥 SSL КРИТИЧНО: $HOSTPORT — $DAYS_LEFT дней" \ "Сертификат истекает $EXPIRE. Срочно продлите!" 10 elif [ "$DAYS_LEFT" -lt "$WARN_DAYS" ]; then /usr/local/bin/notifly-send \ "⚠️ SSL: $HOSTPORT истекает через $DAYS_LEFT дн." \ "Срок действия до $EXPIRE." 5 fidonesudo chmod +x /usr/local/bin/notifly-cert-checkOnce a day at noon:
0 12 * * * root /usr/local/bin/notifly-cert-check \ notifly.ru \ api.example.com \ mail.example.com:993Non-standard ports are supported (host:port).
Alternative: checking the local file
Section titled “Alternative: checking the local file”If certbot stores certificates in /etc/letsencrypt/live/*/fullchain.pem,
you can check the file directly (faster, does not depend on network):
for f in /etc/letsencrypt/live/*/fullchain.pem; do DOMAIN=$(basename "$(dirname "$f")") EXP_TS=$(date -d "$(openssl x509 -noout -enddate -in "$f" | cut -d= -f2)" +%s) DAYS=$(( (EXP_TS - $(date +%s)) / 86400 )) [ "$DAYS" -lt 14 ] && /usr/local/bin/notifly-send \ "⚠️ SSL: $DOMAIN — $DAYS дн." "Файл: $f" 5doneWindows: PowerShell + Task Scheduler
Section titled “Windows: PowerShell + Task Scheduler”PowerShell can check certificates without openssl — using
System.Net.Sockets.TcpClient + SslStream. It uses the common function
Send-Notifly from the sysadmin/index template.
param([string[]]$Hosts = @("notifly.ru:443", "api.example.com:443")). C:\scripts\Notifly.ps1
$WarnDays = 14$CritDays = 3
foreach ($hp in $Hosts) { $parts = $hp.Split(":"); $h = $parts[0]; $p = if ($parts.Count -gt 1) { [int]$parts[1] } else { 443 } try { $tcp = [System.Net.Sockets.TcpClient]::new() $tcp.Connect($h, $p) $ssl = [System.Net.Security.SslStream]::new( $tcp.GetStream(), $false, { param($s,$c,$ch,$er) $true }) $ssl.AuthenticateAsClient($h) $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($ssl.RemoteCertificate) $daysLeft = ($cert.NotAfter - (Get-Date)).Days $ssl.Dispose(); $tcp.Dispose()
if ($daysLeft -lt $CritDays) { Send-Notifly -Title "🔥 SSL КРИТИЧНО: $hp — $daysLeft дн." ` -Message "Сертификат истекает $($cert.NotAfter.ToString('yyyy-MM-dd')). Issuer: $($cert.Issuer)" ` -Priority 10 } elseif ($daysLeft -lt $WarnDays) { Send-Notifly -Title "⚠️ SSL: $hp истекает через $daysLeft дн." ` -Message "Срок до $($cert.NotAfter.ToString('yyyy-MM-dd'))." ` -Priority 5 } } catch { Send-Notifly -Title "🚨 SSL: не смог проверить $hp" ` -Message "$_" -Priority 8 }}You can also check certificates in the Windows local store (LocalMachine\My):
Get-ChildItem Cert:\LocalMachine\My | Where-Object { ($_.NotAfter - (Get-Date)).Days -lt 14} | ForEach-Object { $days = ($_.NotAfter - (Get-Date)).Days Send-Notifly -Title "⚠️ SSL: $($_.Subject) — $days дн." ` -Message "Thumbprint: $($_.Thumbprint)" -Priority 5}Schedule — once a day:
$Action = New-ScheduledTaskAction -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-Cert-Check.ps1"$Trigger = New-ScheduledTaskTrigger -Daily -At 12:00$Princ = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel HighestRegister-ScheduledTask -TaskName "Notifly Cert Check" ` -Action $Action -Trigger $Trigger -Principal $PrincBenefits
Section titled “Benefits”- You can’t “forget” about the certificate. A notification arrives 2 weeks before, and then every day until you fix it.
- Covers more than just Let’s Encrypt. Paid certificates with annual renewals are especially risky — they get forgotten.
- Checks the actual canonical URL, not just a file — so you can see that nginx is actually serving the correct certificate.
What to improve next
Section titled “What to improve next”- Include the issuer name in the message (
Let's Encrypt R3vsGlobalSign). - Store state, like in the disk recipe, so you don’t spam the same warning every day.
- Also check the CA certificates in the chain.