diff --git a/tools/deploy-ssl-cert.py b/tools/deploy-ssl-cert.py new file mode 100644 index 0000000..8caadb3 --- /dev/null +++ b/tools/deploy-ssl-cert.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +""" +SSL 证书申请和配置脚本 - 在服务器上执行 +Author: Emotion Museum Team +Created: 2026-03-18 +Purpose: 使用 certbot 申请 Let's Encrypt SSL 证书并配置自动续期 + +使用方法: + python3 deploy-ssl-cert.py # 申请证书 + python3 deploy-ssl-cert.py --verify # 验证证书 + python3 deploy-ssl-cert.py --renew # 手动续期 +""" + +import os +import sys +import subprocess +import argparse +from datetime import datetime + +# 配置 +DOMAIN = "lifescript.happylifeos.com" +EMAIL = "admin@happylifeos.com" # 请根据实际情况修改 +CERT_PATH = f"/etc/letsencrypt/live/{DOMAIN}" +NGINX_SSL_CONF = "/etc/nginx/sites-available/lifescript.happylifeos.com.conf" + +# 颜色定义 +RED = '\033[0;31m' +GREEN = '\033[0;32m' +YELLOW = '\033[1;33m' +BLUE = '\033[0;34m' +NC = '\033[0m' + +def log_info(msg): + print(f"{GREEN}[INFO]{NC} {msg}") + +def log_warn(msg): + print(f"{YELLOW}[WARN]{NC} {msg}") + +def log_error(msg): + print(f"{RED}[ERROR]{NC} {msg}") + +def log_section(msg): + print("") + print(f"{BLUE}{'='*60}{NC}") + print(f"{BLUE}{msg}{NC}") + print(f"{BLUE}{'='*60}{NC}") + print("") + +def run_command(cmd, capture=False): + """执行 shell 命令""" + try: + if capture: + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return result.returncode, result.stdout, result.stderr + else: + result = subprocess.run(cmd, shell=True) + return result.returncode, None, None + except Exception as e: + log_error(f"命令执行失败:{e}") + return 1, None, str(e) + +def check_certbot(): + """检查 certbot 是否已安装""" + log_info("检查 certbot 是否已安装...") + returncode, stdout, stderr = run_command("which certbot", capture=True) + if returncode == 0: + log_info(f"certbot 已安装:{stdout.strip()}") + return True + else: + log_warn("certbot 未安装,尝试安装...") + return install_certbot() + +def install_certbot(): + """安装 certbot""" + log_info("正在安装 certbot...") + + # 检查系统类型 + if os.path.exists("/etc/debian_version") or os.path.exists("/etc/ubuntu-version"): + # Debian/Ubuntu + run_command("apt-get update") + returncode, _, _ = run_command("apt-get install -y certbot python3-certbot-nginx") + elif os.path.exists("/etc/redhat-release") or os.path.exists("/etc/centos-release"): + # CentOS/RHEL + returncode, _, _ = run_command("yum install -y certbot python3-certbot-nginx") + else: + # 尝试通用安装方法 + returncode, _, _ = run_command("snap install --classic certbot") + + if returncode == 0: + log_info("certbot 安装成功") + return True + else: + log_error("certbot 安装失败,请手动安装") + return False + +def check_existing_cert(): + """检查是否已有证书""" + if os.path.exists(f"{CERT_PATH}/fullchain.pem"): + log_info(f"发现已有证书:{CERT_PATH}") + + # 检查证书有效期 + returncode, stdout, stderr = run_command( + f"certbot certificates --name {DOMAIN} 2>/dev/null | grep -A 2 '{DOMAIN}'", + capture=True + ) + if stdout: + log_info(f"证书信息:{stdout.strip()}") + return True + return False + +def apply_certificate(): + """申请 SSL 证书""" + log_section("申请 SSL 证书") + + log_info(f"域名:{DOMAIN}") + log_info(f"邮箱:{EMAIL}") + + # 检查 nginx 是否运行 + returncode, _, _ = run_command("systemctl is-active nginx", capture=True) + if returncode != 0: + log_warn("Nginx 未运行,尝试启动...") + run_command("systemctl start nginx") + + # 使用 standalone 模式申请证书(不需要 nginx 配置) + log_info("使用 standalone 模式申请证书...") + cmd = ( + f"certbot certonly " + f"--standalone " + f"-d {DOMAIN} " + f"--email {EMAIL} " + f"--agree-tos " + f"--non-interactive " + f"--force-renewal" + ) + + log_info(f"执行命令:{cmd}") + returncode, stdout, stderr = run_command(cmd) + + if returncode == 0: + log_info("✅ SSL 证书申请成功") + log_info(f"证书路径:{CERT_PATH}") + return True + else: + log_error(f"证书申请失败:{stderr}") + + # 尝试使用 nginx 插件 + log_warn("尝试使用 nginx 插件模式...") + cmd = ( + f"certbot --nginx " + f"-d {DOMAIN} " + f"--email {EMAIL} " + f"--agree-tos " + f"--non-interactive " + f"--force-renewal" + ) + returncode, stdout, stderr = run_command(cmd) + + if returncode == 0: + log_info("✅ SSL 证书申请成功(nginx 模式)") + return True + else: + log_error(f"证书申请失败:{stderr}") + return False + +def setup_auto_renewal(): + """配置自动续期""" + log_section("配置自动续期") + + # 检查是否已存在定时任务 + returncode, stdout, stderr = run_command("crontab -l 2>/dev/null | grep certbot", capture=True) + if "certbot renew" in stdout: + log_info("自动续期任务已存在") + return True + + # 添加定时任务(每天凌晨 2 点检查) + log_info("添加 certbot 自动续期定时任务...") + + cron_job = "0 2 * * * /usr/bin/certbot renew --quiet --deploy-hook 'systemctl reload nginx'" + + # 备份并添加新的 cron 任务 + run_command("(crontab -l 2>/dev/null | grep -v certbot; echo '%s') | crontab -" % cron_job) + + # 验证 + returncode, stdout, stderr = run_command("crontab -l | grep certbot", capture=True) + if "certbot" in stdout: + log_info("✅ 自动续期任务已配置") + log_info(f"任务:{stdout.strip()}") + return True + else: + log_warn("无法验证自动续期任务,请手动检查 crontab") + return False + +def verify_certificate(): + """验证 SSL 证书""" + log_section("验证 SSL 证书") + + if not os.path.exists(f"{CERT_PATH}/fullchain.pem"): + log_error(f"证书文件不存在:{CERT_PATH}/fullchain.pem") + return False + + # 检查证书有效期 + log_info("检查证书信息...") + cmd = f"openssl x509 -in {CERT_PATH}/fullchain.pem -noout -dates -subject" + returncode, stdout, stderr = run_command(cmd, capture=True) + + if returncode == 0: + log_info(f"证书信息:\n{stdout}") + + # 检查是否过期 + returncode, stdout, stderr = run_command( + f"openssl x509 -in {CERT_PATH}/fullchain.pem -noout -checkend 0", + capture=True + ) + if returncode == 0: + log_info("✅ 证书有效") + else: + log_error("❌ 证书已过期") + return False + + # 验证域名匹配 + returncode, stdout, stderr = run_command( + f"openssl x509 -in {CERT_PATH}/fullchain.pem -noout -subject -ext subjectAltName", + capture=True + ) + if DOMAIN in stdout: + log_info(f"✅ 证书域名匹配:{DOMAIN}") + else: + log_error(f"❌ 证书域名不匹配:{DOMAIN}") + return False + + return True + else: + log_error(f"验证失败:{stderr}") + return False + +def renew_certificate(): + """手动续期证书""" + log_section("手动续期 SSL 证书") + + cmd = "certbot renew --force-renewal --deploy-hook 'systemctl reload nginx'" + log_info(f"执行命令:{cmd}") + + returncode, stdout, stderr = run_command(cmd) + + if returncode == 0: + log_info("✅ 证书续期成功") + return True + else: + log_error(f"证书续期失败:{stderr}") + return False + +def print_summary(): + """打印总结""" + log_section("SSL 证书配置完成") + + log_info("📋 证书信息:") + log_info(f" 域名:{DOMAIN}") + log_info(f" 证书路径:{CERT_PATH}") + log_info(f" 证书文件:fullchain.pem, privkey.pem") + + log_info("🔧 验证命令:") + log_info(f" python3 deploy-ssl-cert.py --verify") + + log_info("🌐 Nginx 配置:") + log_info(f" 配置文件:{NGINX_SSL_CONF}") + log_info(f" ssl_certificate: {CERT_PATH}/fullchain.pem") + log_info(f" ssl_certificate_key: {CERT_PATH}/privkey.pem") + + log_info("⏰ 自动续期:") + log_info(f" crontab -l | grep certbot") + + print("") + +def main(): + parser = argparse.ArgumentParser(description="SSL 证书申请和配置工具") + parser.add_argument("--verify", action="store_true", help="验证 SSL 证书") + parser.add_argument("--renew", action="store_true", help="手动续期证书") + args = parser.parse_args() + + log_section("情绪博物馆 - SSL 证书管理") + log_info(f"时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + # 检查是否为 root 用户 + if os.geteuid() != 0: + log_error("请使用 root 用户或 sudo 执行此脚本") + sys.exit(1) + + if args.verify: + # 验证模式 + success = verify_certificate() + sys.exit(0 if success else 1) + + elif args.renew: + # 续期模式 + success = renew_certificate() + print_summary() + sys.exit(0 if success else 1) + + else: + # 申请模式 + if not check_certbot(): + log_error("certbot 未安装,无法继续") + sys.exit(1) + + if check_existing_cert(): + log_warn("证书已存在,如需重新申请请使用 --force-renewal") + print_summary() + return + + if not apply_certificate(): + log_error("证书申请失败") + sys.exit(1) + + if not setup_auto_renewal(): + log_warn("自动续期配置失败,请手动配置") + + verify_certificate() + print_summary() + +if __name__ == "__main__": + main()