From aa1e12c6b99224eb41aa00c4414860931972685c Mon Sep 17 00:00:00 2001 From: Peanut Date: Tue, 17 Mar 2026 23:52:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20SSL=20=E8=AF=81?= =?UTF-8?q?=E4=B9=A6=E7=94=B3=E8=AF=B7=E4=B8=8E=E8=87=AA=E5=8A=A8=E7=BB=AD?= =?UTF-8?q?=E6=9C=9F=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 certbot 申请 Let's Encrypt SSL 证书 - 自动配置 nginx HTTPS - 支持 systemd timer 或 cron 自动续期 - 提供证书状态验证功能 - 支持 dry-run 模拟运行模式 Author: emotion-museum Created: 2026-03-17 --- tools/deploy-ssl-cert.py | 222 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 tools/deploy-ssl-cert.py diff --git a/tools/deploy-ssl-cert.py b/tools/deploy-ssl-cert.py new file mode 100644 index 0000000..a43072a --- /dev/null +++ b/tools/deploy-ssl-cert.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +SSL 证书申请与自动续期脚本 +使用 certbot 工具申请 Let's Encrypt 证书并配置 nginx + +Author: emotion-museum +Created: 2026-03-17 +Purpose: SSL 证书申请与自动续期 - 使用 certbot 配置 Let's Encrypt 证书 +""" + +import argparse +import subprocess +import sys +from pathlib import Path + +DOMAIN = "lifescript.happylifeos.com" +CERT_PATH = f"/etc/letsencrypt/live/{DOMAIN}/fullchain.pem" +KEY_PATH = f"/etc/letsencrypt/live/{DOMAIN}/privkey.pem" + + +def log_info(msg): + """输出信息日志""" + print(f"\033[32m[INFO]\033[0m {msg}") + + +def log_warn(msg): + """输出警告日志""" + print(f"\033[33m[WARN]\033[0m {msg}") + + +def log_error(msg): + """输出错误日志""" + print(f"\033[31m[ERROR]\033[0m {msg}") + + +def run_command(cmd, check=True): + """执行 shell 命令 + + Args: + cmd: 要执行的命令 + check: 是否检查返回码,False 时总是返回 True + + Returns: + bool: 命令是否执行成功 + """ + try: + result = subprocess.run( + cmd, shell=True, check=check, capture_output=False, text=True + ) + return result.returncode == 0 + except subprocess.CalledProcessError as e: + log_error(f"命令执行失败:{e}") + return False + + +def check_certbot(): + """检查 certbot 是否安装 + + Returns: + bool: certbot 是否已安装 + """ + log_info("检查 certbot 是否安装...") + if not run_command("which certbot", check=False): + log_error("certbot 未安装,请先安装:apt install certbot python3-certbot-nginx") + return False + log_info("certbot 已安装") + return True + + +def check_nginx(): + """检查 nginx 配置是否正确 + + Returns: + bool: nginx 配置是否正确 + """ + log_info("检查 nginx 配置...") + if not run_command("nginx -t", check=False): + log_error("nginx 配置有误,请先修复") + return False + log_info("nginx 配置正确") + return True + + +def apply_certificate(dry_run=False): + """申请 SSL 证书 + + Args: + dry_run: 是否使用 dry-run 模式 + + Returns: + bool: 证书申请是否成功 + """ + log_info(f"开始为 {DOMAIN} 申请 SSL 证书...") + + cmd = f"certbot --nginx -d {DOMAIN} --non-interactive --agree-tos --email admin@happylifeos.com" + if dry_run: + cmd += " --dry-run" + log_info(f"[DRY RUN] 执行:{cmd}") + + if run_command(cmd): + log_info("证书申请成功") + return True + else: + log_error("证书申请失败") + return False + + +def setup_auto_renewal(): + """配置自动续期 + + Returns: + bool: 配置是否成功 + """ + log_info("配置证书自动续期...") + + # 检查是否已存在 systemd timer + if run_command("systemctl list-timers | grep certbot", check=False): + log_info("certbot 自动续期 timer 已存在") + return True + + # 启用 systemd timer + if run_command("systemctl enable certbot.timer && systemctl start certbot.timer"): + log_info("已启用 certbot 自动续期 timer") + return True + else: + log_warn("systemd timer 配置失败,尝试配置 cron 任务") + return setup_cron_renewal() + + +def setup_cron_renewal(): + """配置 cron 自动续期 + + Returns: + bool: cron 配置是否成功 + """ + cron_job = "0 3 1 * * certbot renew --quiet --deploy-hook 'systemctl reload nginx'" + log_info(f"添加 cron 任务:{cron_job}") + + # 添加到 crontab + result = subprocess.run( + f"(crontab -l 2>/dev/null | grep -v '{DOMAIN}'; echo '{cron_job}') | crontab -", + shell=True, capture_output=True, text=True + ) + + if result.returncode == 0: + log_info("cron 任务添加成功") + return True + else: + log_error("cron 任务添加失败") + return False + + +def verify_certificate(): + """验证证书是否有效 + + Returns: + bool: 证书是否有效 + """ + log_info("验证 SSL 证书...") + cmd = f'echo | openssl s_client -connect {DOMAIN}:443 -servername {DOMAIN} 2>/dev/null | openssl x509 -noout -dates' + + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + if result.returncode == 0: + log_info(f"证书信息:\n{result.stdout}") + return True + else: + log_warn("无法验证证书(可能域名 DNS 尚未生效)") + return False + + +def main(): + """主函数 + + Returns: + int: 退出码,0 表示成功,非 0 表示失败 + """ + parser = argparse.ArgumentParser(description="SSL 证书申请与自动续期脚本") + parser.add_argument("--dry-run", action="store_true", help="模拟运行,不实际申请证书") + parser.add_argument("--renew", action="store_true", help="手动续期证书") + parser.add_argument("--verify", action="store_true", help="验证证书状态") + parser.add_argument("--setup-renewal", action="store_true", help="仅配置自动续期") + args = parser.parse_args() + + # 验证模式 + if args.verify: + verify_certificate() + return 0 + + # 仅配置自动续期 + if args.setup_renewal: + setup_auto_renewal() + return 0 + + # 手动续期 + if args.renew: + run_command("certbot renew --force-renewal") + run_command("systemctl reload nginx") + return 0 + + # 完整流程 + log_info("=== SSL 证书申请流程 ===") + + if not check_certbot(): + return 1 + + if not check_nginx(): + return 1 + + if not apply_certificate(dry_run=args.dry_run): + return 1 + + if not args.dry_run: + setup_auto_renewal() + verify_certificate() + + log_info("=== SSL 证书申请完成 ===") + return 0 + + +if __name__ == "__main__": + sys.exit(main())