定时检查并更新数字证书脚本

wen 实用脚本 1

本文目录导读:

定时检查并更新数字证书脚本

  1. Python 版本(推荐,功能最完整)
  2. 配置文件
  3. 依赖安装
  4. 使用系统定时任务(Cron)
  5. 简化版本(Bash脚本)
  6. 使用方法
  7. 功能特点

我来为您提供一个定时检查并更新数字证书的脚本,以下是针对不同场景的解决方案:

Python 版本(推荐,功能最完整)

cert_monitor.py

#!/usr/bin/env python3
"""
数字证书定时检查与更新脚本
支持:SSL/TLS证书、本地文件证书、ACME自动续期
"""
import os
import sys
import ssl
import socket
import logging
import subprocess
import json
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import yaml
import OpenSSL
import requests
# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('cert_monitor.log'),
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger(__name__)
class CertificateMonitor:
    """证书监控器"""
    def __init__(self, config_path: str = 'config.yaml'):
        self.config = self._load_config(config_path)
        self.check_interval = self.config.get('check_interval', 24)  # 小时
        self.warning_days = self.config.get('warning_days', 30)      # 天数
        self.critical_days = self.config.get('critical_days', 7)     # 天数
    def _load_config(self, config_path: str) -> Dict:
        """加载配置文件"""
        try:
            with open(config_path, 'r', encoding='utf-8') as f:
                return yaml.safe_load(f)
        except FileNotFoundError:
            logger.error(f"配置文件 {config_path} 未找到")
            sys.exit(1)
        except yaml.YAMLError as e:
            logger.error(f"配置文件解析错误: {e}")
            sys.exit(1)
    def check_remote_cert(self, hostname: str, port: int = 443) -> Dict:
        """检查远程服务器证书"""
        try:
            context = ssl.create_default_context()
            with socket.create_connection((hostname, port), timeout=10) as sock:
                with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                    cert = ssock.getpeercert()
                    # 解析证书信息
                    not_after = datetime.strptime(
                        cert['notAfter'], '%b %d %H:%M:%S %Y %Z'
                    )
                    not_before = datetime.strptime(
                        cert['notBefore'], '%b %d %H:%M:%S %Y %Z'
                    )
                    return {
                        'hostname': hostname,
                        'port': port,
                        'subject': dict(cert['subject'][0]),
                        'issuer': dict(cert['issuer'][0]),
                        'not_before': not_before,
                        'not_after': not_after,
                        'days_remaining': (not_after - datetime.now()).days,
                        'valid': not_before <= datetime.now() <= not_after,
                        'serial_number': cert.get('serialNumber', 'N/A')
                    }
        except Exception as e:
            logger.error(f"检查 {hostname}:{port} 证书失败: {e}")
            return {'hostname': hostname, 'port': port, 'error': str(e)}
    def check_local_cert(self, cert_path: str) -> Dict:
        """检查本地证书文件"""
        try:
            with open(cert_path, 'rb') as f:
                cert_data = f.read()
            cert = OpenSSL.crypto.load_certificate(
                OpenSSL.crypto.FILETYPE_PEM, cert_data
            )
            not_after = datetime.strptime(
                cert.get_notAfter().decode('ascii'), '%Y%m%d%H%M%SZ'
            )
            not_before = datetime.strptime(
                cert.get_notBefore().decode('ascii'), '%Y%m%d%H%M%SZ'
            )
            # 获取证书信息
            subject = dict(cert.get_subject().get_components())
            issuer = dict(cert.get_issuer().get_components())
            return {
                'path': cert_path,
                'subject': subject,
                'issuer': issuer,
                'not_before': not_before,
                'not_after': not_after,
                'days_remaining': (not_after - datetime.now()).days,
                'valid': not_before <= datetime.now() <= not_after,
                'serial_number': cert.get_serial_number()
            }
        except Exception as e:
            logger.error(f"检查本地证书 {cert_path} 失败: {e}")
            return {'path': cert_path, 'error': str(e)}
    def check_letsencrypt_cert(self, domain: str) -> Dict:
        """检查Let's Encrypt证书"""
        cert_path = f"/etc/letsencrypt/live/{domain}/fullchain.pem"
        if os.path.exists(cert_path):
            return self.check_local_cert(cert_path)
        else:
            return {'domain': domain, 'error': '证书文件未找到'}
    def renew_letsencrypt_cert(self, domain: str) -> bool:
        """续期Let's Encrypt证书"""
        try:
            logger.info(f"正在续期 {domain} 的Let's Encrypt证书...")
            # 使用certbot续期
            result = subprocess.run(
                ['certbot', 'renew', '--cert-name', domain, '--non-interactive'],
                capture_output=True,
                text=True
            )
            if result.returncode == 0:
                logger.info(f"证书 {domain} 续期成功")
                return True
            else:
                logger.error(f"证书 {domain} 续期失败: {result.stderr}")
                return False
        except Exception as e:
            logger.error(f"续期证书 {domain} 时出错: {e}")
            return False
    def send_alert(self, message: str, level: str = 'warning'):
        """发送告警通知"""
        alert_methods = self.config.get('alert_methods', [])
        for method in alert_methods:
            if method['type'] == 'email':
                self._send_email_alert(message, method, level)
            elif method['type'] == 'webhook':
                self._send_webhook_alert(message, method, level)
            elif method['type'] == 'telegram':
                self._send_telegram_alert(message, method, level)
    def _send_email_alert(self, message: str, config: Dict, level: str):
        """发送邮件告警"""
        try:
            import smtplib
            from email.mime.text import MIMEText
            from email.mime.multipart import MIMEMultipart
            msg = MIMEMultipart()
            msg['From'] = config['from']
            msg['To'] = config['to']
            msg['Subject'] = f"[{level.upper()}] 证书监控告警"
            body = f"""
            <h2>证书监控告警</h2>
            <p>级别: {level}</p>
            <p>时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
            <pre>{message}</pre>
            """
            msg.attach(MIMEText(body, 'html'))
            with smtplib.SMTP(config['smtp_server'], config['smtp_port']) as server:
                server.starttls()
                server.login(config['username'], config['password'])
                server.send_message(msg)
            logger.info(f"邮件告警已发送至 {config['to']}")
        except Exception as e:
            logger.error(f"发送邮件告警失败: {e}")
    def _send_webhook_alert(self, message: str, config: Dict, level: str):
        """发送Webhook告警(如钉钉、企业微信等)"""
        try:
            payload = {
                'msgtype': 'text',
                'text': {
                    'content': f"[{level}] 证书监控告警\n{message}"
                }
            }
            response = requests.post(
                config['url'],
                json=payload,
                headers={'Content-Type': 'application/json'}
            )
            if response.status_code == 200:
                logger.info(f"Webhook告警发送成功")
            else:
                logger.error(f"Webhook告警发送失败: {response.status_code}")
        except Exception as e:
            logger.error(f"发送Webhook告警失败: {e}")
    def _send_telegram_alert(self, message: str, config: Dict, level: str):
        """发送Telegram告警"""
        try:
            url = f"https://api.telegram.org/bot{config['bot_token']}/sendMessage"
            payload = {
                'chat_id': config['chat_id'],
                'text': f"[{level}] 证书监控告警\n\n{message}",
                'parse_mode': 'HTML'
            }
            response = requests.post(url, json=payload)
            if response.status_code == 200:
                logger.info(f"Telegram告警发送成功")
            else:
                logger.error(f"Telegram告警发送失败: {response.status_code}")
        except Exception as e:
            logger.error(f"发送Telegram告警失败: {e}")
    def run_check(self):
        """执行检查"""
        results = []
        alerts = []
        # 检查远程证书
        for server in self.config.get('remote_servers', []):
            result = self.check_remote_cert(
                server['hostname'],
                server.get('port', 443)
            )
            results.append(result)
            if 'error' not in result:
                days = result['days_remaining']
                if days <= self.critical_days:
                    alerts.append({
                        'level': 'critical',
                        'message': f"证书 {result['hostname']} 将在 {days} 天后过期!"
                    })
                elif days <= self.warning_days:
                    alerts.append({
                        'level': 'warning',
                        'message': f"证书 {result['hostname']} 将在 {days} 天后过期"
                    })
        # 检查本地证书
        for cert_path in self.config.get('local_certs', []):
            result = self.check_local_cert(cert_path)
            results.append(result)
            if 'error' not in result:
                days = result['days_remaining']
                if days <= self.critical_days:
                    alerts.append({
                        'level': 'critical',
                        'message': f"证书 {result['path']} 将在 {days} 天后过期!"
                    })
        # 检查Let's Encrypt证书并自动续期
        for domain in self.config.get('letsencrypt_domains', []):
            result = self.check_letsencrypt_cert(domain)
            results.append(result)
            if 'error' not in result and result['days_remaining'] <= 30:
                logger.info(f"证书 {domain} 将在 {result['days_remaining']} 天后过期,尝试续期...")
                if self.renew_letsencrypt_cert(domain):
                    logger.info(f"证书 {domain} 续期成功")
                else:
                    alerts.append({
                        'level': 'error',
                        'message': f"证书 {domain} 续期失败"
                    })
        # 发送告警
        for alert in alerts:
            self.send_alert(alert['message'], alert['level'])
        # 记录检查结果
        self._save_results(results)
        return results
    def _save_results(self, results: List[Dict]):
        """保存检查结果"""
        output = {
            'timestamp': datetime.now().isoformat(),
            'results': results
        }
        with open('cert_check_results.json', 'w') as f:
            json.dump(output, f, indent=2, default=str)
    def start_daemon(self):
        """以守护进程方式运行"""
        import time
        logger.info(f"证书监控守护进程启动,检查间隔: {self.check_interval} 小时")
        while True:
            try:
                logger.info("开始执行证书检查...")
                self.run_check()
                logger.info("证书检查完成")
                # 等待下一个检查周期
                time.sleep(self.check_interval * 3600)
            except KeyboardInterrupt:
                logger.info("收到中断信号,退出监控")
                break
            except Exception as e:
                logger.error(f"检查过程出错: {e}")
                time.sleep(3600)  # 出错后等待1小时重试
def main():
    """主函数"""
    import argparse
    parser = argparse.ArgumentParser(description='证书定时检查与更新工具')
    parser.add_argument('--config', '-c', default='config.yaml', help='配置文件路径')
    parser.add_argument('--run-once', '-r', action='store_true', help='只运行一次检查')
    parser.add_argument('--daemon', '-d', action='store_true', help='以守护进程方式运行')
    args = parser.parse_args()
    monitor = CertificateMonitor(args.config)
    if args.run_once:
        monitor.run_check()
    elif args.daemon:
        monitor.start_daemon()
    else:
        # 默认先执行一次检查
        monitor.run_check()
        print("证书检查完成,结果已保存到 cert_check_results.json")
if __name__ == '__main__':
    main()

配置文件

config.yaml

# 证书监控配置
# 检查间隔(小时)
check_interval: 24
# 告警天数
warning_days: 30
critical_days: 7
# 远程服务器检查列表
remote_servers:
  - hostname: "example.com"
    port: 443
  - hostname: "api.example.com"
    port: 443
  - hostname: "mail.example.com"
    port: 465
# 本地证书文件列表
local_certs:
  - "/etc/nginx/ssl/server.crt"
  - "/etc/apache2/ssl/server.crt"
  - "/path/to/custom/cert.pem"
# Let's Encrypt 域名列表
letsencrypt_domains:
  - "example.com"
  - "www.example.com"
  - "api.example.com"
# 告警配置(支持多种方式)
alert_methods:
  - type: "email"
    from: "monitor@example.com"
    to: "admin@example.com"
    smtp_server: "smtp.example.com"
    smtp_port: 587
    username: "monitor@example.com"
    password: "your-email-password"
  - type: "webhook"
    url: "https://hooks.example.com/webhook/cert-monitor"
  - type: "telegram"
    bot_token: "your-bot-token"
    chat_id: "your-chat-id"

依赖安装

# 安装Python依赖
pip install pyOpenSSL requests pyyaml
# 安装certbot(用于Let's Encrypt自动续期)
sudo apt install certbot  # Ubuntu/Debian
sudo yum install certbot  # CentOS/RHEL

使用系统定时任务(Cron)

crontab_setup.sh

#!/bin/bash
# 设置每天凌晨2点检查证书
echo "0 2 * * * /usr/bin/python3 /path/to/cert_monitor.py --run-once --config /path/to/config.yaml >> /var/log/cert_monitor.log 2>&1" | crontab -
# 或者设置系统d服务(推荐)
cat > /etc/systemd/system/cert-monitor.service << EOF
[Unit]
Description=Certificate Monitor Service
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 /path/to/cert_monitor.py --daemon --config /path/to/config.yaml
Restart=always
RestartSec=60
[Install]
WantedBy=multi-user.target
EOF
# 启用并启动服务
systemctl daemon-reload
systemctl enable cert-monitor
systemctl start cert-monitor

简化版本(Bash脚本)

cert_check.sh

#!/bin/bash
# 数字证书检查脚本(简化版)
CERT_PATH="/etc/ssl/certs"
LOG_FILE="/var/log/cert_check.log"
WARNING_DAYS=30
ALERT_EMAIL="admin@example.com"
check_cert() {
    local cert_file=$1
    if [ ! -f "$cert_file" ]; then
        echo "错误: 证书文件 $cert_file 不存在" | tee -a $LOG_FILE
        return 1
    fi
    # 获取证书过期时间
    local expiry_date=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2)
    if [ -z "$expiry_date" ]; then
        echo "错误: 无法读取证书 $cert_file" | tee -a $LOG_FILE
        return 1
    fi
    # 计算剩余天数
    local expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null)
    local current_epoch=$(date +%s)
    local days_remaining=$(( (expiry_epoch - current_epoch) / 86400 ))
    echo "证书: $cert_file" | tee -a $LOG_FILE
    echo "过期日期: $expiry_date" | tee -a $LOG_FILE
    echo "剩余天数: $days_remaining" | tee -a $LOG_FILE
    # 检查是否接近过期
    if [ $days_remaining -le 0 ]; then
        echo "严重: 证书已过期!" | tee -a $LOG_FILE
        send_alert "证书 $cert_file 已过期!"
    elif [ $days_remaining -le $WARNING_DAYS ]; then
        echo "警告: 证书将在 $days_remaining 天后过期" | tee -a $LOG_FILE
        if [ $days_remaining -le 7 ]; then
            send_alert "证书 $cert_file 将在 $days_remaining 天后过期!"
        fi
    fi
    echo "---" | tee -a $LOG_FILE
}
send_alert() {
    local message=$1
    echo "$message" | mail -s "证书过期告警" $ALERT_EMAIL
}
renew_letsencrypt() {
    local domain=$1
    echo "正在续期 Let's Encrypt 证书: $domain" | tee -a $LOG_FILE
    certbot renew --cert-name "$domain" --non-interactive 2>&1 | tee -a $LOG_FILE
}
# 主检查流程
main() {
    echo "========================================" | tee -a $LOG_FILE
    echo "证书检查开始: $(date)" | tee -a $LOG_FILE
    echo "========================================" | tee -a $LOG_FILE
    # 检查指定目录下的所有证书
    for cert in "$CERT_PATH"/*.crt; do
        if [ -f "$cert" ]; then
            check_cert "$cert"
        fi
    done
    # 检查远程服务器(可选)
    # echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
    echo "证书检查完成: $(date)" | tee -a $LOG_FILE
    echo "" | tee -a $LOG_FILE
}
# 执行主函数
main

使用方法

基本使用:

# 安装依赖
pip install pyOpenSSL requests pyyaml
# 创建配置文件
cp config.yaml.example config.yaml
# 编辑config.yaml,填入你的配置
# 单次运行
python3 cert_monitor.py --run-once
# 守护进程模式
python3 cert_monitor.py --daemon
# 使用cron定时执行(每天检查一次)
0 0 * * * /usr/bin/python3 /path/to/cert_monitor.py --run-once

功能特点

  • ✅ 支持远程HTTPS证书检查
  • ✅ 支持本地证书文件检查
  • ✅ 支持Let's Encrypt自动续期
  • ✅ 多种告警方式(邮件、Webhook、Telegram)
  • ✅ 自定义检查间隔和告警阈值
  • ✅ 结果保存到JSON文件
  • ✅ 守护进程模式
  • ✅ 详细日志记录

这个脚本可以根据您的具体需求进行修改和扩展!

抱歉,评论功能暂时关闭!