Skip to content

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.

/usr/local/bin/notifly-cert-check:

#!/usr/bin/env bash
# Usage:
# notifly-cert-check example.com [example.com:8443] [...]
set -eu
set -a; source /etc/notifly.env; set +a
WARN_DAYS=14
CRIT_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
fi
done
Окно терминала
sudo chmod +x /usr/local/bin/notifly-cert-check

Once a day at noon:

0 12 * * * root /usr/local/bin/notifly-cert-check \
notifly.ru \
api.example.com \
mail.example.com:993

Non-standard ports are supported (host:port).

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" 5
done

PowerShell can check certificates without openssl — using System.Net.Sockets.TcpClient + SslStream. It uses the common function Send-Notifly from the sysadmin/index template.

C:\scripts\Notifly-Cert-Check.ps1
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 Highest
Register-ScheduledTask -TaskName "Notifly Cert Check" `
-Action $Action -Trigger $Trigger -Principal $Princ
  • 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.
  • Include the issuer name in the message (Let's Encrypt R3 vs GlobalSign).
  • 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.