Files
happy-life-star/tools/deploy-ssl-cert.py
T
peanut 35126a5144 feat: 创建 SSL 证书申请脚本(服务器端执行)
- 支持 standalone 和 nginx 两种模式申请证书
- 自动配置 certbot 定时任务实现自动续期
- 支持 --verify 验证证书状态
- 支持 --renew 手动续期证书
2026-03-18 19:07:36 +08:00

322 lines
9.7 KiB
Python

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