Уведомления об истечении SSL-сертификатов
Сертификат, который должен был автоматически продлиться через certbot, иногда не продлевается: упал хук, поменялся DNS, истёк токен, кончилось место. Узнать об этом за 14 дней — нормально, узнать за 2 часа — катастрофа.
Скрипт проверки
Заголовок раздела «Скрипт проверки»/usr/local/bin/notifly-cert-check:
#!/usr/bin/env bash# Использование:# 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-checkРаз в день в полдень:
0 12 * * * root /usr/local/bin/notifly-cert-check \ notifly.ru \ api.example.com \ mail.example.com:993Поддерживаются нестандартные порты (host:port).
Альтернатива: проверка локального файла
Заголовок раздела «Альтернатива: проверка локального файла»Если certbot хранит сертификаты в /etc/letsencrypt/live/*/fullchain.pem,
можно проверять файл напрямую (быстрее, не зависит от сети):
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
Заголовок раздела «Windows: PowerShell + Task Scheduler»PowerShell умеет проверять сертификаты без openssl — через
System.Net.Sockets.TcpClient + SslStream. Использует общую функцию
Send-Notifly из шаблона sysadmin/index.
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 }}Также можно проверять сертификаты в локальном хранилище Windows (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}Расписание — раз в сутки:
$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 $Princ- Невозможно «забыть» о сертификате. Уведомление приходит за 2 недели, а потом — каждый день, пока вы не починили.
- Покрывает не только Let’s Encrypt. Платные сертификаты с продлением раз в год особенно опасны — про них забывают.
- Проверяется реальный canonical URL, а не файл — то есть видно, что nginx действительно отдаёт нужный сертификат.
Что улучшить дальше
Заголовок раздела «Что улучшить дальше»- Прикладывать в сообщение название issuer-а (
Let's Encrypt R3vsGlobalSign). - Хранить состояние, как в рецепте про диск, чтобы не спамить ежедневно одним и тем же предупреждением.
- Проверять также CA-сертификаты в цепочке.