feat: 创建 SSL 证书申请脚本(服务器端执行)
- 支持 standalone 和 nginx 两种模式申请证书 - 自动配置 certbot 定时任务实现自动续期 - 支持 --verify 验证证书状态 - 支持 --renew 手动续期证书
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user