Smail/deploy/nginx/renew_certificate.py
smail eb5758bd3b
Some checks are pending
Security Checks / security (push) Waiting to run
修正后台域名与泛域名证书验收
2026-09-22 07:18:43 +08:00

78 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/python3
'''使用显式 DNS 或 Webroot 验证续期,不停止 Nginx,校验通过后才发布。'''
import os
from pathlib import Path
import re
import stat
import subprocess
import deploy_certificate as deploy
ROOT = Path('/var/lib/smail-acme-tls')
DOMAINS = deploy.DOMAINS
RENEW_TIMEOUT = 600
WEBROOT = Path('/var/lib/smail-acme')
def dns_provider():
provider = os.environ.get('SMAIL_ACME_DNS_PROVIDER', '')
if not re.fullmatch(r'[a-z][a-z0-9_]{0,63}', provider) or provider == 'exec':
raise RuntimeError('未配置有效的 DNS 服务商,拒绝续期;不会回退到停机验证')
return provider
def challenge_arguments():
method = os.environ.get('SMAIL_ACME_CHALLENGE', 'dns')
if method == 'dns':
return ['--dns', dns_provider()]
if method != 'http':
raise RuntimeError('验证方式必须为 dns 或 http,不允许自动回退到停机验证')
private = ROOT.lstat()
if (ROOT.resolve(strict=True) != ROOT or not stat.S_ISDIR(private.st_mode)
or private.st_uid != 0 or private.st_mode & 0o077):
raise RuntimeError('ACME 账号目录必须由 root 独占,禁止公开访问')
for directory in (WEBROOT, WEBROOT / '.well-known', WEBROOT / '.well-known/acme-challenge'):
info = directory.lstat()
if (directory.resolve(strict=True) != directory or not stat.S_ISDIR(info.st_mode)
or info.st_uid != 0 or info.st_mode & 0o022
or info.st_mode & 0o005 != 0o005):
raise RuntimeError('HTTP 验证目录必须由 root 管理、可读取且不能被其他用户写入')
return ['--http', '--http.webroot', str(WEBROOT)]
def main():
if os.geteuid() != 0:
raise RuntimeError('必须使用 root 执行证书续期')
with deploy.certificate_lock():
certificate = str(deploy.TARGET / 'custom.crt')
subprocess.run(['openssl', 'x509', '-in', certificate, '-noout'], check=True)
result = subprocess.run(['openssl', 'x509', '-in', certificate, '-noout', '-checkend', '2592000'],
capture_output=True)
if result.returncode not in (0, 1):
raise RuntimeError('无法判断线上证书有效期,拒绝续期')
due = result.returncode == 1
if due:
challenge = challenge_arguments()
deploy.check_nginx_paths()
subprocess.run(['nginx', '-t'], check=True)
subprocess.run(['systemctl', 'is-active', '--quiet', 'nginx'], check=True)
command = ['/usr/local/libexec/smail-acme/lego', '--path', str(ROOT),
'--email', '', '--accept-tos', '--key-type', 'rsa2048', *challenge,
'--server', 'https://acme-v02.api.letsencrypt.org/directory',
'--http-timeout', '20', '--cert.timeout', '30']
for domain in DOMAINS:
command.extend(['--domains', domain])
command.extend(['renew', '--days', '30', '--reuse-key', '--no-random-sleep'])
subprocess.run(command, check=True, timeout=RENEW_TIMEOUT,
env={**os.environ, 'GOMEMLIMIT': '64MiB'},
umask=0o022 if '--http' in challenge else 0o077)
else:
print('线上证书有效期大于 30 天,本次不申请新证书')
deploy.publish()
if __name__ == '__main__':
main()