Smail/deploy/nginx/renew_certificate.py
smail af0cb9b311
Some checks are pending
Security Checks / security (push) Waiting to run
禁止泛域名证书使用HTTP验证
2026-09-22 07:25:49 +08:00

80 lines
3.5 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,不允许自动回退到停机验证')
if '*.solit.cn' in DOMAINS:
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()