Compare commits
9 Commits
488996222b
...
d2cc85bef2
| Author | SHA1 | Date | |
|---|---|---|---|
| d2cc85bef2 | |||
| ac500d0f5b | |||
| e48941efc4 | |||
| d25321e26b | |||
| d0be5048e4 | |||
| 3835490d72 | |||
| 6a413f4334 | |||
| f3eb4283f5 | |||
| 1e19399862 |
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
conf/*.conf
|
||||
.idea
|
||||
__pycache__/
|
||||
venv/
|
||||
.env
|
||||
37
Makefile
Normal file
37
Makefile
Normal file
@ -0,0 +1,37 @@
|
||||
PROJECT := certificate-expire-check
|
||||
USER := $(PROJECT)
|
||||
INSTALL_DIR := /etc/$(PROJECT)
|
||||
CONF_DIR := $(INSTALL_DIR)/conf
|
||||
SCRIPT := $(INSTALL_DIR)/main.py
|
||||
CRON_SCHEDULE:= 0 0 */10 * *
|
||||
|
||||
.PHONY: all user install cron clean
|
||||
|
||||
all: user install cron
|
||||
|
||||
user:
|
||||
@id -u $(USER) &>/dev/null || \
|
||||
sudo useradd --system --no-create-home --shell /usr/sbin/nologin $(USER) && \
|
||||
echo "User '$(USER)' has been created."
|
||||
@echo "User '$(USER)' already exists - skip it."
|
||||
|
||||
# Copy the project to /etc and set permissions
|
||||
install: user
|
||||
sudo mkdir -p $(INSTALL_DIR)
|
||||
sudo cp -r ./* $(INSTALL_DIR)
|
||||
sudo chown -R $(USER):$(USER) $(INSTALL_DIR)
|
||||
@echo "The project is installed in $(INSTALL_DIR) and permissions are assigned to user '$(USER)'."
|
||||
|
||||
# Add a cron job as a user
|
||||
cron: install
|
||||
@CRON_LINE="$(CRON_SCHEDULE) /usr/bin/python3 $(SCRIPT) >> /var/log/$(PROJECT).log 2>&1"
|
||||
# add CRON_LINE in user crontab if not exists
|
||||
@sudo crontab -u $(USER) -l 2>/dev/null | grep -F "$$CRON_LINE" >/dev/null || \
|
||||
( sudo crontab -u $(USER) -l 2>/dev/null; echo "$$CRON_LINE" ) | sudo crontab -u $(USER) -
|
||||
@echo "Cron entry for user '$(USER)' is set: $$CRON_LINE"
|
||||
|
||||
clean:
|
||||
-sudo crontab -u $(USER) -r
|
||||
-sudo userdel $(USER)
|
||||
-sudo rm -rf $(INSTALL_DIR)
|
||||
@echo The cleanup is complete."
|
||||
76
README.md
Normal file
76
README.md
Normal file
@ -0,0 +1,76 @@
|
||||
# certificate-expire-check
|
||||
|
||||
A simple Python utility to monitor SSL/TLS certificate expiration for a list of domains and send notifications via email.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── conf/
|
||||
│ ├── mail.conf # SMTP configuration
|
||||
│ └── domains.txt # List of domains and optional ports
|
||||
├── main.py # Entry-point script
|
||||
├── modules/
|
||||
│ ├── __init__.py
|
||||
│ ├── config.py # Configuration loader
|
||||
│ ├── domains.py # Domain list parser
|
||||
│ ├── checker.py # Certificate expiration checker
|
||||
│ └── notifier.py # SMTP notifier
|
||||
├── Makefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Python 3.9+
|
||||
* tzdata
|
||||
* make(optional)
|
||||
* Root or sudo privileges for auto installation and cron setup with Makefile
|
||||
|
||||
## Configuration
|
||||
|
||||
1. **SMTP Settings**: Edit `conf/mail.conf`:
|
||||
|
||||
2. **Domain List**: Edit `conf/domains.txt`, one domain per line. Append `:port` to specify a custom port (default is 443):
|
||||
|
||||
```
|
||||
example.com:443
|
||||
google.com
|
||||
expired.badssl.com:8443
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
From the project root, run:
|
||||
|
||||
```bash
|
||||
sudo make all
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
1. Create a system user `certificate-expire-check` if it does not exist
|
||||
2. Copy project files to `/etc/certificate-expire-check`
|
||||
3. Set ownership to the `certificate-expire-check` user
|
||||
4. Install a cron job that runs every 10 days at midnight
|
||||
|
||||
## Makefile Targets
|
||||
|
||||
* **all**: Runs `user`, `install`, and `cron` in sequence
|
||||
* **user**: Creates the system user
|
||||
* **install**: Copies files and sets permissions
|
||||
* **cron**: Adds the following cron entry for the user:
|
||||
|
||||
```cron
|
||||
0 0 */10 * * /usr/bin/python3 /etc/certificate-expire-check/main.py >> /var/log/certificate-expire-check.log 2>&1
|
||||
```
|
||||
* **clean**: Removes the cron job, deletes the system user, and removes the installation directory
|
||||
|
||||
## Usage
|
||||
|
||||
* Logs are written to `/var/log/certificate-expire-check.log`.
|
||||
* Manually trigger the check with:
|
||||
|
||||
```bash
|
||||
sudo -u certificate-expire-check /usr/bin/python3 /etc/certificate-expire-check/main.py
|
||||
```
|
||||
4
conf/domains.conf.sample
Normal file
4
conf/domains.conf.sample
Normal file
@ -0,0 +1,4 @@
|
||||
example.com:443
|
||||
google.com
|
||||
expired.badssl.com:8443
|
||||
|
||||
8
conf/mail.conf.sample
Normal file
8
conf/mail.conf.sample
Normal file
@ -0,0 +1,8 @@
|
||||
[smtp]
|
||||
host = smtp.example.com
|
||||
port = 587
|
||||
username = user@example.com
|
||||
password = S3cr3tP@ss
|
||||
from = monitor@example.com
|
||||
to = admin@example.com
|
||||
|
||||
82
main.py
82
main.py
@ -1,56 +1,44 @@
|
||||
import ssl
|
||||
import socket
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
|
||||
MOSCOW_TZ = ZoneInfo("Europe/Moscow")
|
||||
from modules.config import load_mail_config
|
||||
from modules.domains import load_domains
|
||||
from modules.checker import get_cert_expiry_utc
|
||||
from modules.notifier import send_email
|
||||
|
||||
def get_cert_expiry_utc(host: str, port: int = 443) -> datetime:
|
||||
ctx = ssl.create_default_context()
|
||||
with ctx.wrap_socket(socket.socket(), server_hostname=host) as sock:
|
||||
sock.settimeout(5)
|
||||
sock.connect((host, port))
|
||||
cert = sock.getpeercert()
|
||||
exp_str = cert['notAfter']
|
||||
dt_utc = datetime.strptime(exp_str, '%b %d %H:%M:%S %Y %Z')
|
||||
return dt_utc.replace(tzinfo=ZoneInfo("UTC"))
|
||||
# Пути к файлам
|
||||
BASE_DIR = Path(__file__).parent
|
||||
CONF_DIR = BASE_DIR / 'conf'
|
||||
MAIL_CONF = CONF_DIR / 'mail.conf'
|
||||
DOMAINS_FILE = CONF_DIR / 'domains.conf'
|
||||
|
||||
def send_email(subject: str, body: str, to_addr: str) -> None:
|
||||
msg = EmailMessage()
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = 'monitor@example.com'
|
||||
msg['To'] = to_addr
|
||||
msg.set_content(body)
|
||||
with smtplib.SMTP('smtp.example.com') as smtp:
|
||||
smtp.login('user', 'pass')
|
||||
smtp.send_message(msg)
|
||||
# Настройки
|
||||
MOSCOW_TZ = ZoneInfo('Europe/Moscow')
|
||||
WARNING_DAYS = 10
|
||||
|
||||
if __name__ == '__main__':
|
||||
domain = 'example.com'
|
||||
|
||||
# 1. Берём время истечения в UTC
|
||||
expiry_utc = get_cert_expiry_utc(domain)
|
||||
|
||||
# 2. Конвертируем expiry в московское время для вывода
|
||||
expiry_moscow = expiry_utc.astimezone(MOSCOW_TZ)
|
||||
|
||||
# 3. Получаем текущее время в Москве
|
||||
def main():
|
||||
# Загружаем настройки и список доменов
|
||||
mail_cfg = load_mail_config(MAIL_CONF)
|
||||
domains = load_domains(DOMAINS_FILE)
|
||||
now_moscow = datetime.now(MOSCOW_TZ)
|
||||
|
||||
# 4. Считаем дни до окончания
|
||||
days_left = (expiry_moscow.date() - now_moscow.date()).days
|
||||
for host, port in domains:
|
||||
expiry_utc = get_cert_expiry_utc(host, port)
|
||||
expiry_local = expiry_utc.astimezone(MOSCOW_TZ)
|
||||
days_left = (expiry_local.date() - now_moscow.date()).days
|
||||
|
||||
if days_left < 30:
|
||||
subject = f'Сертификат {domain} истекает через {days_left} дней'
|
||||
body = (
|
||||
f'Сертификат домена {domain} истечёт {expiry_moscow:%Y-%m-%d %H:%M:%S %Z}.\n'
|
||||
f'Осталось {days_left} дней (по московскому времени).'
|
||||
)
|
||||
#send_email(subject, body, 'admin@example.com')
|
||||
body = (
|
||||
f'Сертификат домена {domain} истечёт {expiry_moscow:%Y-%m-%d %H:%M:%S %Z}.\n'
|
||||
f'Осталось {days_left} дней (по московскому времени).'
|
||||
)
|
||||
print(body)
|
||||
print(f"{host}:{port} — истекает {expiry_local:%Y-%m-%d %H:%M:%S %Z}, осталось {days_left} дн.")
|
||||
|
||||
if days_left < WARNING_DAYS:
|
||||
subject = f"[WARNING] SSL {host} истекает через {days_left} дн."
|
||||
body = (
|
||||
f"Домен: {host}\n"
|
||||
f"Порт: {port}\n"
|
||||
f"Истекает: {expiry_local:%Y-%m-%d %H:%M:%S %Z}\n"
|
||||
f"Осталось дней: {days_left}\n"
|
||||
)
|
||||
send_email(mail_cfg, subject, body)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
18
modules/checker.py
Normal file
18
modules/checker.py
Normal file
@ -0,0 +1,18 @@
|
||||
import ssl
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
from typing import Tuple
|
||||
|
||||
def get_cert_expiry_utc(host: str, port: int = 443) -> datetime:
|
||||
"""
|
||||
Устанавливает TLS-соединение и возвращает дату окончания сертификата
|
||||
в UTC (timezone-aware).
|
||||
"""
|
||||
ctx = ssl.create_default_context()
|
||||
with ctx.wrap_socket(socket.socket(), server_hostname=host) as sock:
|
||||
sock.settimeout(5)
|
||||
sock.connect((host, port))
|
||||
cert = sock.getpeercert()
|
||||
exp_str = cert['notAfter'] # 'Jun 15 12:00:00 2025 GMT'
|
||||
dt = datetime.strptime(exp_str, '%b %d %H:%M:%S %Y %Z')
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
20
modules/config.py
Normal file
20
modules/config.py
Normal file
@ -0,0 +1,20 @@
|
||||
import configparser
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
def load_mail_config(path: Path) -> Dict[str, str]:
|
||||
"""
|
||||
Читает mail.conf и возвращает словарь с настройками SMTP:
|
||||
host, port, username, password, from, to
|
||||
"""
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding='utf-8')
|
||||
smtp = cfg['smtp']
|
||||
return {
|
||||
'host': smtp.get('host'),
|
||||
'port': smtp.getint('port', fallback=587),
|
||||
'username': smtp.get('username'),
|
||||
'password': smtp.get('password'),
|
||||
'from': smtp.get('from'),
|
||||
'to': smtp.get('to'),
|
||||
}
|
||||
19
modules/domains.py
Normal file
19
modules/domains.py
Normal file
@ -0,0 +1,19 @@
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
def load_domains(path: Path) -> List[Tuple[str, int]]:
|
||||
"""
|
||||
Возвращает список кортежей (host, port).
|
||||
Если порт не указан — 443.
|
||||
"""
|
||||
result: List[Tuple[str, int]] = []
|
||||
for line in path.read_text(encoding='utf-8').splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if ':' in line:
|
||||
host, port = line.split(':', 1)
|
||||
result.append((host.strip(), int(port)))
|
||||
else:
|
||||
result.append((line, 443))
|
||||
return result
|
||||
19
modules/notifier.py
Normal file
19
modules/notifier.py
Normal file
@ -0,0 +1,19 @@
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
from typing import Dict
|
||||
|
||||
def send_email(cfg: Dict[str, str], subject: str, body: str) -> None:
|
||||
"""
|
||||
Отправляет письмо через SMTP по настройкам из cfg.
|
||||
Ожидает ключи: host, port, username, password, from, to.
|
||||
"""
|
||||
msg = EmailMessage()
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = cfg['from']
|
||||
msg['To'] = cfg['to']
|
||||
msg.set_content(body)
|
||||
|
||||
with smtplib.SMTP(cfg['host'], cfg['port']) as smtp:
|
||||
smtp.starttls()
|
||||
smtp.login(cfg['username'], cfg['password'])
|
||||
smtp.send_message(msg)
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
tzdata
|
||||
Loading…
x
Reference in New Issue
Block a user