feat: 添加 SSL 证书申请与自动续期脚本
- 使用 certbot 申请 Let's Encrypt SSL 证书 - 自动配置 nginx HTTPS - 支持 systemd timer 或 cron 自动续期 - 提供证书状态验证功能 - 支持 dry-run 模拟运行模式 Author: emotion-museum Created: 2026-03-17
This commit is contained in:
@@ -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())
|
||||||
Reference in New Issue
Block a user