Перевірка TLS-сертифікатів Python – це вузький скрипт для контролю строку дії сертифікатів на списку хостів. Раз на день він підключається до endpoint, читає notAfter і шле alert, якщо сертифікат скоро завершиться або перевірка впала. Для 5-20 зовнішніх сервісів цього зазвичай достатньо.
Такий варіант підходить, коли потрібен саме контроль expiry, а не повний моніторинг доступності. Скрипт можна покласти на будь-який Linux-сервер або VPS. Межа теж очевидна: історії перевірок, UI і складної логіки сповіщень тут немає.
Робочий Python-скрипт з читанням notAfter
Мінімальний придатний варіант відкриває TLS-з’єднання з SNI, читає notAfter, рахує дні до завершення і збирає лише проблемні хости. SNI тут критичний: без нього на shared-хостингу часто повертається чужий сертифікат.
У цьому і сенс перевірки TLS-сертифікатів Python: список хостів лежить у JSON, поріг днів приходить з аргументу командного рядка, а канал сповіщення – зі змінних середовища. Код не доводиться правити при кожній зміні списку або способу доставки alert.
#!/usr/bin/env python3
import argparse
import json
import os
import smtplib
import socket
import ssl
import sys
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from email.message import EmailMessage
def load_hosts(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("hosts file must contain a JSON array")
return data
def get_cert_expiry_days(hostname, port, timeout):
context = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=timeout) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
not_after = cert["notAfter"]
expiry = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
delta = expiry - now
return delta.days, expiry.isoformat()
def send_telegram(message):
token = os.environ.get("TELEGRAM_BOT_TOKEN")
chat_id = os.environ.get("TELEGRAM_CHAT_ID")
if not token or not chat_id:
return False, "Telegram env vars are missing"
url = f"https://api.telegram.org/bot{token}/sendMessage"
data = urllib.parse.urlencode({
"chat_id": chat_id,
"text": message
}).encode("utf-8")
req = urllib.request.Request(url, data=data, method="POST")
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status != 200:
return False, f"Telegram HTTP {resp.status}"
return True, "Telegram sent"
def send_email(message, subject):
host = os.environ.get("SMTP_HOST")
port = int(os.environ.get("SMTP_PORT", "587"))
user = os.environ.get("SMTP_USER")
password = os.environ.get("SMTP_PASS")
sender = os.environ.get("SMTP_FROM")
recipient = os.environ.get("SMTP_TO")
if not all([host, user, password, sender, recipient]):
return False, "SMTP env vars are missing"
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = sender
msg["To"] = recipient
msg.set_content(message)
with smtplib.SMTP(host, port, timeout=10) as smtp:
smtp.starttls()
smtp.login(user, password)
smtp.send_message(msg)
return True, "Email sent"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--hosts-file", default="/opt/tls-check/hosts.json")
parser.add_argument("--threshold-days", type=int, default=21)
parser.add_argument("--timeout", type=int, default=5)
parser.add_argument("--notify", choices=["telegram", "email", "both", "none"], default="telegram")
args = parser.parse_args()
try:
hosts = load_hosts(args.hosts_file)
except Exception as e:
print(f"Failed to load hosts file: {e}", file=sys.stderr)
sys.exit(2)
problems = []
for item in hosts:
hostname = item["host"]
port = int(item.get("port", 443))
try:
days_left, expiry = get_cert_expiry_days(hostname, port, args.timeout)
if days_left <= args.threshold_days:
problems.append(f"{hostname}:{port} - {days_left} days left - expires {expiry}")
except Exception as e:
problems.append(f"{hostname}:{port} - check failed - {e}")
if not problems:
print("OK: no certificates below threshold")
sys.exit(0)
body = "TLS certificate alerts:\n\n" + "\n".join(problems)
print(body)
if args.notify in ("telegram", "both"):
ok, status = send_telegram(body)
print(status)
if args.notify in ("email", "both"):
ok, status = send_email(body, "TLS certificate alert")
print(status)
sys.exit(1)
if __name__ == "__main__":
main()
Скрипт завершується кодом 0, якщо проблем немає, і 1, якщо є хоча б один alert. Це спрощує інтеграцію з cron, systemd і зовнішнім моніторингом.
Як передати список хостів і поріг без редагування коду
Найпростіше винести список у JSON, а поріг днів передавати аргументом --threshold-days. Для кількох десятків записів цього вистачає. YAML або база даних тут не дають виграшу, якщо не потрібні теги, групи чи окремі правила на кожен хост.
Створіть каталог і файл зі списком перевірок:
sudo mkdir -p /opt/tls-check sudo nano /opt/tls-check/hosts.json
Вміст /opt/tls-check/hosts.json:
[
{ "host": "example.com", "port": 443 },
{ "host": "grafana.example.com", "port": 443 },
{ "host": "mail.example.com", "port": 8443 }
]
Запуск із порогом 30 днів:
python3 /opt/tls-check/check_tls.py --hosts-file /opt/tls-check/hosts.json --threshold-days 30 --notify none
Окремі порти потрібні не для рідкісних випадків. Якщо HTTPS сидить за реверс-проксі на 8443 або іншому порту, запит має йти саме туди. Якщо схема побудована через HTTPS через реверс-проксі, перевіряти варто зовнішню точку входу, а не внутрішній контейнер без публічного сертифіката.
Alert у Telegram або email
Сповіщення варто надсилати лише коли сертифікат скоро завершиться або перевірка впала. Постійні повідомлення про успіх швидко стають шумом. Після цього критичний alert пропускають так само, як і все інше.
Telegram через Bot API
Для Telegram достатньо токена бота і chat_id. Канал зручний тим, що піднімається швидко і не залежить від локального SMTP.
export TELEGRAM_BOT_TOKEN="<bot-token>" export TELEGRAM_CHAT_ID="<chat-id>" python3 /opt/tls-check/check_tls.py --hosts-file /opt/tls-check/hosts.json --threshold-days 365 --notify telegram
Поріг 365 днів у тесті майже гарантовано форсує сповіщення. Якщо потрібна окрема схема для месенджера, підійде матеріал про Telegram-сповіщення в Uptime Kuma. Для цієї задачі власний скрипт простіший.
Email через SMTP
Email доречний там, де Telegram не підходить або alert треба відправляти в спільну скриньку. Критичний момент один: код використовує STARTTLS через smtplib.SMTP, тому параметри SMTP мають збігатися з режимом сервера.
export SMTP_HOST="smtp.example.com" export SMTP_PORT="587" export SMTP_USER="[email protected]" export SMTP_PASS="<smtp-password>" export SMTP_FROM="[email protected]" export SMTP_TO="[email protected]" python3 /opt/tls-check/check_tls.py --hosts-file /opt/tls-check/hosts.json --threshold-days 365 --notify email
Якщо сервер працює лише на implicit TLS через 465, код доведеться змінити на smtplib.SMTP_SSL. Саме тут такі скрипти часто сипляться після копіювання без перевірки.
cron чи systemd timer для щоденного запуску
cron підходить для одного скрипта без додаткової логіки. systemd timer зручніший, коли потрібні окремий юніт, журнал у journalctl і простий ручний запуск. Для домашнього сервера або невеликого VPS обидва варіанти нормальні.
Запуск через cron
Спочатку краще винести змінні середовища в окремий файл. Тримати токени прямо в crontab незручно і брудно.
sudo nano /opt/tls-check/tls-check.env
Приклад /opt/tls-check/tls-check.env:
TELEGRAM_BOT_TOKEN=<bot-token> TELEGRAM_CHAT_ID=<chat-id>
Права на файл краще обмежити:
sudo chmod 600 /opt/tls-check/tls-check.env
Запис у crontab:
0 8 * * * . /opt/tls-check/tls-check.env; /usr/bin/python3 /opt/tls-check/check_tls.py --hosts-file /opt/tls-check/hosts.json --threshold-days 21 --notify telegram >> /var/log/tls-check.log 2>&1
Запуск через systemd timer
Коли вже є моніторинг домашнього сервера, systemd зазвичай зручніший. Видно історію запусків, коди завершення і текст помилок без окремого лог-файла.
Файл сервісу /etc/systemd/system/tls-check.service:
[Unit] Description=TLS certificate check [Service] Type=oneshot EnvironmentFile=/opt/tls-check/tls-check.env ExecStart=/usr/bin/python3 /opt/tls-check/check_tls.py --hosts-file /opt/tls-check/hosts.json --threshold-days 21 --notify telegram
Файл таймера /etc/systemd/system/tls-check.timer:
[Unit] Description=Run TLS certificate check daily [Timer] OnCalendar=daily Persistent=true [Install] WantedBy=timers.target
Активація:
sudo systemctl daemon-reload sudo systemctl enable --now tls-check.timer sudo systemctl list-timers --all | grep tls-check
Як перевірити, що скрипт бачить дату сертифіката і шле повідомлення
Перевірка TLS-сертифікатів Python має тестуватися окремими кроками. Спершу треба перевірити TCP-з’єднання до хоста, потім розбір notAfter, а вже після цього доставку alert. Інакше незрозуміло, де саме поломка.
Тест без сповіщень
Спочатку запустіть скрипт без відправки повідомлень:
python3 /opt/tls-check/check_tls.py --hosts-file /opt/tls-check/hosts.json --threshold-days 21 --notify none echo $?
Код 0 означає, що проблемних сертифікатів немає. Код 1 означає близьке завершення або помилку перевірки. Для цього сценарію це штатна поведінка.
Форсований alert
Щоб перевірити доставку, задайте завищений поріг:
python3 /opt/tls-check/check_tls.py --hosts-file /opt/tls-check/hosts.json --threshold-days 365 --notify telegram
Для email достатньо замінити канал на email або both. Такий тест потрібен завжди, бо TLS-перевірка може бути коректною, а SMTP або Bot API – ні.
Додаткова звірка через openssl
Якщо дата виводиться дивно або хост падає з помилкою, варто звірити результат напряму:
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates
Цей виклик швидко показує дві типові причини: неправильний SNI або помилковий порт. Якщо зовнішній доступ побудований через Cloudflare Tunnel на Debian і Ubuntu, перевіряти треба публічний hostname, який бачить клієнт.
Де зазвичай ламається перевірка
Більшість збоїв зводиться до трьох причин: не передали server_hostname, поставили занадто малий timeout або перевіряється не той endpoint. На одному-двох хостах це може не вилізти. На списку з десятка адрес помилки починають повторюватися системно.
Без SNI повертається чужий сертифікат
На shared IP один сервер часто обслуговує кілька доменів. Без server_hostname=hostname у wrap_socket() OpenSSL може віддати дефолтний сертифікат іншого сайту. Строк дії тоді читається коректно, але належить не тому домену.
Малий timeout дає випадкові падіння
Значення 2-3 секунди інколи вистачає в LAN. Для зовнішніх хостів цього часто мало. Якщо адреси сидять за CDN або тунелем, краще ставити 5-10 секунд, інакше скрипт почне плодити шумові timeout-и.
На нестандартному порту перевіряють не той endpoint
Таке часто трапляється з проксі та панелями керування. Зовнішній сертифікат може жити на 443, а внутрішній сервіс – на 8443 з іншим ланцюжком або самопідписаним сертифікатом. Перевіряти треба адресу, куди реально приходить клієнтський трафік.
До кількох десятків хостів цей підхід живе спокійно. Далі повільні вузли починають розтягувати загальний час проходу. У цей момент або додають паралельні перевірки, або переносять задачу в окремий моніторинг.
Коли скрипта достатньо, а коли потрібен Uptime Kuma
Перевірка TLS-сертифікатів Python підходить, коли потрібен один вузький контроль: строк дії сертифіката на кількох хостах і простий alert. Uptime Kuma кращий, коли потрібні веб-інтерфейс, історія, статус-сторінка, кілька каналів сповіщення і перевірки HTTP, TCP та DNS.
| Сценарій | Python-скрипт | Uptime Kuma |
|---|---|---|
| 5-20 хостів, контроль expiry | Підходить | Надлишково |
| Потрібен Telegram або email alert | Підходить | Підходить |
| Історія перевірок і UI | Доведеться доробляти | Є з коробки |
| Моніторинг доступності, а не лише сертифікатів | Слабкий варіант | Кращий вибір |
Плюс скрипта в тому, що весь ланцюжок видно одразу: куди він підключається, що читає і чому шле alert. Але після появи mute-вікон, кількох каналів, історії інцидентів і додаткових перевірок самописний код швидко починає заважати. У цій точці простіше взяти Uptime Kuma.
Для вузької задачі перевірка TLS-сертифікатів Python закриває потребу без зайвої інфраструктури. Якщо треба стежити ще й за доступністю сервісів, реверс-проксі та домашніх веб-панелей, скрипт краще залишити як точкову перевірку, а решту віддати окремій системі.
Коментарі
Коментарів ще немає. Обговорення - у Telegram.