#!/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()