Перейти к содержимому

Уведомления об истечении SSL-сертификатов

Сертификат, который должен был автоматически продлиться через certbot, иногда не продлевается: упал хук, поменялся DNS, истёк токен, кончилось место. Узнать об этом за 14 дней — нормально, узнать за 2 часа — катастрофа.

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

#!/usr/bin/env bash
# Использование:
# 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

Раз в день в полдень:

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

PowerShell умеет проверять сертификаты без openssl — через System.Net.Sockets.TcpClient + SslStream. Использует общую функцию Send-Notifly из шаблона sysadmin/index.

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
}
}

Также можно проверять сертификаты в локальном хранилище 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 Highest
Register-ScheduledTask -TaskName "Notifly Cert Check" `
-Action $Action -Trigger $Trigger -Principal $Princ
  • Невозможно «забыть» о сертификате. Уведомление приходит за 2 недели, а потом — каждый день, пока вы не починили.
  • Покрывает не только Let’s Encrypt. Платные сертификаты с продлением раз в год особенно опасны — про них забывают.
  • Проверяется реальный canonical URL, а не файл — то есть видно, что nginx действительно отдаёт нужный сертификат.
  • Прикладывать в сообщение название issuer-а (Let's Encrypt R3 vs GlobalSign).
  • Хранить состояние, как в рецепте про диск, чтобы не спамить ежедневно одним и тем же предупреждением.
  • Проверять также CA-сертификаты в цепочке.