之前宝塔自动续签失效两次: 宝塔不能自动续签的 bug 修复
本以为已经好了,直到今天又失效,算你厉害,用不起。
更新宝塔还是没用,坑爹。
我不理解: 比续签更复杂你们都能做,怎么到续签就出问题了。
不仅仅我一个人续签失败,挺多人都是这样。
我理解不了啊,你们是不是故意的???
给 ai 的:
我要自动续签 nginx 的证书,服务器是用的宝塔。
1. 有个 domains 变量,是一个列表
域名有:
- c.com
- www.a.com b.com
- a-admin.com v.xx.com ...
2. http 请求所有域名,根据域名证书是否小于 30 天,小于 30 天判定为过期。
3. 利用/home/xxx/acme.sh 来申请证书,使用阿里云的 DNS 解析。AccessKey:xxx SecretKey:123456
4. 最后更新到 nginx 。
模型用的是 auto-select
,给了屎一样的代码。
还说我 python 版低(我 3.12.3 ),也不知道用啥模型了,手动选择 3.7 才能用。
改下配置就能用
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import ssl
import socket
import datetime
import subprocess
import os
import time
from typing import List, Tuple
# 域名列表
domains = [
"a.com,www.a.com",
"admin.b.com,x.b.com",
"c.com"
]
# 阿里云 DNS 配置
ALIYUN_ACCESS_KEY = "xxx"
ALIYUN_SECRET_KEY = "xx"
def check_cert_expiry(domain: str) -> Tuple[bool, int]:
"""
检查证书是否过期
返回: (是否过期, 剩余天数)
对于多域名证书,检查每个域名并返回最短的剩余天数
"""
try:
# 处理多域名情况,逗号分隔的域名
if ',' in domain:
domains_list = domain.split(',')
min_days_left = float('inf') # 设置初始值为无穷大
all_results = []
# 检查每个域名
for single_domain in domains_list:
single_domain = single_domain.strip()
expired, days = check_cert_expiry(single_domain)
all_results.append((single_domain, expired, days))
if days < min_days_left:
min_days_left = days
# 打印所有域名的结果
for single_domain, expired, days in all_results:
print(f" - 子域名 {single_domain} 剩余天数: {days}")
# 如果最小天数小于 30 ,则需要续签
return min_days_left < 30, min_days_left
# 使用外部命令获取证书信息
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:
print(f"检查域名 {domain} 证书时出错: 无法连接或获取证书")
return True, 0
# 解析输出找到过期日期
output = result.stdout
not_after_line = [line for line in output.splitlines() if line.startswith('notAfter=')]
if not not_after_line:
print(f"检查域名 {domain} 证书时出错: 无法获取过期时间")
return True, 0
# 解析日期格式,例如: notAfter=May 30 12:00:00 2023 GMT
date_str = not_after_line[0].split('=')[1]
expires_date = datetime.datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z')
days_left = (expires_date - datetime.datetime.now()).days
print(f"域名 {domain} 证书到期日期: {expires_date.strftime('%Y-%m-%d')}, 剩余天数: {days_left}")
return days_left < 30, days_left
except Exception as e:
print(f"检查域名 {domain} 证书时出错: {str(e)}")
return True, 0 # 如果无法检查,默认为需要续签
def set_ali_env():
"""
设置阿里云 DNS API 的环境变量
"""
os.environ['Ali_Key'] = ALIYUN_ACCESS_KEY
os.environ['Ali_Secret'] = ALIYUN_SECRET_KEY
def check_dns_record_exists(domain: str) -> bool:
"""
检查指定域名的 DNS 验证记录是否存在
"""
try:
# 设置环境变量
set_ali_env()
# 验证记录的域名前缀
acme_challenge = f"_acme-challenge.{domain}"
# 使用阿里云 CLI 查询记录
cmd = f"aliyun alidns DescribeDomainRecords --DomainName {domain.split('.')[-2]}.{domain.split('.')[-1]} --RRKeyWord _acme-challenge --Type TXT"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
# 检查输出中是否包含记录
return acme_challenge in result.stdout
except Exception as e:
print(f"检查 DNS 记录时出错: {str(e)}")
# 如果无法确定,假设记录存在,以确保安全
return True
def renew_cert(domain: str) -> bool:
"""
使用 acme.sh 续签证书
支持多域名证书申请
"""
try:
# 先设置环境变量
set_ali_env()
acme_path = "/home/xxx/acme.sh"
# 确保 acme.sh 有执行权限
os.chmod(acme_path, 0o755)
# 处理多域名情况
domain_params = ""
main_domain = ""
if ',' in domain:
domains_list = domain.split(',')
main_domain = domains_list[0].strip()
domain_params = f"-d {main_domain}"
# 添加其他域名
for alt_domain in domains_list[1:]:
alt_domain = alt_domain.strip()
domain_params += f" -d {alt_domain}"
else:
main_domain = domain
domain_params = f"-d {domain}"
# 检查并清理 DNS 记录
needs_cleanup = False
# 检查主域名
if check_dns_record_exists(main_domain):
print(f"域名 {main_domain} 存在 DNS 验证记录,需要清理")
needs_cleanup = True
# 清理主域名
cleanup_cmd = f"{acme_path}/acme.sh --cleanup --domain {main_domain} --dns dns_ali"
print(f"执行清理命令: {cleanup_cmd}")
cleanup_process = subprocess.run(cleanup_cmd, shell=True, capture_output=True, text=True)
print(f"清理结果: {cleanup_process.stdout}")
else:
print(f"域名 {main_domain} 不存在 DNS 验证记录,无需清理")
# 检查其他域名
if ',' in domain:
for alt_domain in domain.split(',')[1:]:
alt_domain = alt_domain.strip()
if check_dns_record_exists(alt_domain):
print(f"域名 {alt_domain} 存在 DNS 验证记录,需要清理")
needs_cleanup = True
# 清理其他域名
alt_cleanup_cmd = f"{acme_path}/acme.sh --cleanup --domain {alt_domain} --dns dns_ali"
print(f"执行清理命令: {alt_cleanup_cmd}")
alt_cleanup_process = subprocess.run(alt_cleanup_cmd, shell=True, capture_output=True, text=True)
print(f"清理结果: {alt_cleanup_process.stdout}")
else:
print(f"域名 {alt_domain} 不存在 DNS 验证记录,无需清理")
# 如果进行了清理,等待 DNS 记录更新
if needs_cleanup:
print("等待 DNS 记录清理完成...")
time.sleep(30) # 等待 30 秒确保 DNS 记录已清理
# 执行续签命令,明确指定使用 Let's Encrypt
cmd = f"{acme_path}/acme.sh --issue --dns dns_ali {domain_params} --keylength 2048 --force --dnssleep 120 --server letsencrypt"
print(f"执行命令: {cmd}")
process = subprocess.Popen(
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# 获取输出
stdout, stderr = process.communicate()
if process.returncode == 0:
print(f"续签输出: {stdout}")
return True
else:
print(f"续签错误: {stderr}")
# 如果仍然失败,尝试完全移除证书再重新申请
if "DNS record already exists" in stderr:
print("尝试完全移除证书后重新申请...")
# 移除证书
for d in domain.split(','):
d = d.strip()
remove_cmd = f"{acme_path}/acme.sh --remove -d {d}"
print(f"执行移除命令: {remove_cmd}")
subprocess.run(remove_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 再次等待
print("等待 DNS 记录更新...")
time.sleep(30)
# 重新申请
reissue_cmd = f"{acme_path}/acme.sh --issue --dns dns_ali {domain_params} --keylength 2048 --force --dnssleep 180 --server letsencrypt"
print(f"执行重新申请命令: {reissue_cmd}")
reissue_process = subprocess.Popen(
reissue_cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
reissue_stdout, reissue_stderr = reissue_process.communicate()
if reissue_process.returncode == 0:
print(f"重新申请成功: {reissue_stdout}")
return True
else:
print(f"重新申请失败: {reissue_stderr}")
return False
return False
except Exception as e:
print(f"续签域名 {domain} 证书时出错: {str(e)}")
return False
def deploy_cert(domain: str) -> bool:
"""
部署证书到 Nginx
支持多域名证书部署
"""
try:
acme_path = "/home/xxx/acme.sh"
# 处理多域名情况,使用第一个域名作为主域名
main_domain = domain.split(',')[0].strip() if ',' in domain else domain
# 证书安装路径
nginx_cert_path = f"/www/server/panel/vhost/cert/{main_domain}"
# 确保目录存在
os.makedirs(nginx_cert_path, exist_ok=True)
# 部署证书
cmd = f"{acme_path}/acme.sh --install-cert -d {main_domain} " \
f"--key-file {nginx_cert_path}/privkey.pem " \
f"--fullchain-file {nginx_cert_path}/fullchain.pem "
# f"\ --reloadcmd 'service nginx force-reload'" 利用宝塔重启,而不是 acme.sh 重启
print(f"执行命令: {cmd}")
process = subprocess.Popen(
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# 获取输出
stdout, stderr = process.communicate()
if process.returncode == 0:
print(f"部署输出: {stdout}")
return True
else:
print(f"部署错误: {stderr}")
return False
except Exception as e:
print(f"部署域名 {domain} 证书时出错: {str(e)}")
return False
def update_nginx():
"""
更新 Nginx 配置并重启服务
"""
try:
# 使用宝塔命令重载 Nginx
print("重载 Nginx 配置...")
reload_cmd = "bt reload nginx"
reload_process = subprocess.Popen(
reload_cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
reload_stdout, reload_stderr = reload_process.communicate()
if reload_process.returncode != 0:
print(f"Nginx 重载错误: {reload_stderr}")
return False
# 完全重启 Nginx 以确保证书生效
print("重启 Nginx 服务...")
restart_cmd = "bt restart nginx"
restart_process = subprocess.Popen(
restart_cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
restart_stdout, restart_stderr = restart_process.communicate()
if restart_process.returncode == 0:
print(f"Nginx 重启成功: {restart_stdout}")
return True
else:
print(f"Nginx 重启错误: {restart_stderr}")
return False
except Exception as e:
print(f"更新和重启 Nginx 时出错: {str(e)}")
return False
def main():
print(f"开始检查证书状态 - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
domains_to_renew = []
# 检查所有域名的证书状态
for domain in domains:
print(f"检查域名: {domain}")
is_expired, days_left = check_cert_expiry(domain)
if is_expired:
print(f"域名 {domain} 证书将在 {days_left} 天后过期,需要续签")
domains_to_renew.append(domain)
else:
print(f"域名 {domain} 证书还有 {days_left} 天过期,无需续签")
if not domains_to_renew:
print("所有证书都在有效期内,无需续签")
return
# 续签需要更新的证书
renewed_domains = []
for domain in domains_to_renew:
print(f"\n 正在续签域名 {domain} 的证书...")
if renew_cert(domain):
print(f"域名 {domain} 证书续签成功")
# 部署证书
if deploy_cert(domain):
print(f"域名 {domain} 证书部署成功")
renewed_domains.append(domain)
else:
print(f"域名 {domain} 证书部署失败")
else:
print(f"域名 {domain} 证书续签失败")
# 如果有证书被续签并部署,更新 Nginx 配置
if renewed_domains:
print("\n 正在更新 Nginx 配置...")
if update_nginx():
print("Nginx 配置更新成功")
else:
print("Nginx 配置更新失败")
print(f"\n 证书续签任务完成 - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"已续签的域名: {', '.join(renewed_domains) if renewed_domains else '无'}")
def force_renew_all():
"""
强制更新所有域名的证书,用于测试
"""
print(f"开始强制更新所有证书 - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 续签所有域名的证书
renewed_domains = []
for domain in domains:
print(f"\n 正在更新域名 {domain} 的证书...")
if renew_cert(domain):
print(f"域名 {domain} 证书更新成功")
# 部署证书
if deploy_cert(domain):
print(f"域名 {domain} 证书部署成功")
renewed_domains.append(domain)
else:
print(f"域名 {domain} 证书部署失败")
else:
print(f"域名 {domain} 证书更新失败")
# 如果有证书被更新并部署,更新 Nginx 配置
if renewed_domains:
print("\n 正在更新 Nginx 配置...")
if update_nginx():
print("Nginx 配置更新成功")
else:
print("Nginx 配置更新失败")
print(f"\n 证书更新任务完成 - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"已更新的域名: {', '.join(renewed_domains) if renewed_domains else '无'}")
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == '--force':
force_renew_all()
else:
main()
1
HangoX 1 天前
是会失败,很傻逼
|
![]() |
3
javalaw2010 1 天前
直接 acme.sh ,我这边生产环境稳定跑好几年了。
|
![]() |
4
shangfabao 1 天前
同意楼上,这不重复造轮子么 acme.sh 很稳定
|
6
maximdx 1 天前
letsencrypt 不是有 certbot 吗,用那个不好么
|
7
MangK 1 天前
caddy 自带证书管理,连续签都免了
|
10
Logtous 1 天前
caddy +1 省事
|
![]() |
11
daimaosix 1 天前
certd 配好不用管了
|
![]() |
12
lepig 1 天前
9.0.0 稳定版就是有问题。
> |-没有找到 30 天内到期的 SSL 证书,正在尝试去寻找其它可续签证书! > |-所有任务已处理完成! 就算还有 1 天到期,他依然扫描不到要续签的证书。 现在用最新正式版 9.5 好像可以。 不过我也不打算用面板自带的了 |
13
dnsjia 1 天前
太复杂了,用下我写的这个 https://ssl.dnsjia.com
|
![]() |
15
ripperdev 1 天前
最近用 Caddy 替换了 Nginx ,证书申请和续签不需要额外的配置,省事多了
|
![]() |
16
ttlive 1 天前
用 certd 续签
|
![]() |
18
bronyakaka 1 天前
用 certbot 插件,
全自动配置 nginx ,啥脚本都不用写 |
![]() |
19
skiy 1 天前
|
![]() |
20
jqtmviyu 1 天前
|
![]() |
21
lc5900 1 天前
Caddy +1,包括我的通配符域名证书都一起自动管理了,解放双手
|
![]() |
24
xiangyuecn 1 天前
2025 年了,公网上的 nginx 、apache 之类的 web 服务器还是没有提供自动管理 https 证书的功能吗,整合一下不难吧,方便广大的小网站免去 https 维护,自动根据配置域名 自动通过 ACME 协议一个域名更新个单域名证书就 ojbk 了,url 文件验证对于 web 服务器要多方便就有多方便
|
![]() |
25
WhatTheBridgeSay 1 天前
|
![]() |
26
WhatTheBridgeSay 1 天前
|
27
RobinHuuu 1 天前 via iPhone
acme 的 cron job 就是用来续签的,,,
|
28
colorbeta 1 天前
cf 配置 15 年不是一劳永逸么
|
![]() |
30
skiy 1 天前
@WhatTheBridgeSay 用 shell 判断文件是否有更新,有更新就 reload 行了啊。就一个 shell 脚本而已。每次我迁移时,只需要打包 out ssl 和 acme.sh docker-compose.yml 就行了。迁移非常方便。
|
31
ljpCN 1 天前
k8s 里 cert-manager 可以直接配置续签证书。或者直接用 dokploy 这种部署方案,都比安装宝塔面板更优雅更可扩展。
|
32
root71370 1 天前 via Android
1panel 很简单
|
![]() |
33
jaylee4869 1 天前 ![]() certbot + crontab. 两三行 shell 就好了啊。。
|
![]() |
34
ch3nbo 21 小时 4 分钟前 via Android
Caddy +1 不要太爽,时间用来专注干别的吧。
|
![]() |
35
Ansen 20 小时 42 分钟前 via iPhone
|
36
yangth 13 小时 10 分钟前 via Android
完全没必要 docker ,还要外部脚本去调,一个证书闹麻了
|
![]() |
37
nuk 12 小时 8 分钟前
加个 dns hook 脚本增加删除 dns 记录就得了。。
|
![]() |
38
snylonue 11 小时 16 分钟前
|
39
jenson47 10 小时 56 分钟前
建议这个 https://github.com/usual2970/certimate
支持多个云 |
![]() |
40
zoharSoul 4 小时 34 分钟前
谁会故意啊 别太抽象
|