Smail/deploy/nginx/deploy_certificate.py
smail 46a767c8d9
Some checks are pending
Security Checks / security (push) Waiting to run
部署Nginx反代与可信证书并补充远程验收和续期保护
2026-09-17 23:01:35 +08:00

88 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
"""校验证书并同步至 Smail,私钥与回退副本仅在服务器保存。"""
from datetime import datetime, timezone
import hashlib
import os
from pathlib import Path
import pwd
import shutil
import subprocess
import uuid
DOMAINS = ('solit.cn', 'mail.solit.cn', 'smtp.solit.cn', 'imap.solit.cn', 'pop.solit.cn')
LINEAGE = Path('/var/lib/smail-acme-tls/certificates')
TARGET = Path('/opt/smail/ssl')
def run(*args, **kwargs):
return subprocess.run(args, check=True, capture_output=True, **kwargs).stdout
def replace_file(path, data, uid, gid):
staged = path.with_name(path.name + '.new-' + uuid.uuid4().hex)
fd = os.open(staged, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, 'wb') as stream:
os.fchown(stream.fileno(), uid, gid)
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
os.replace(staged, path)
fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(fd)
finally:
os.close(fd)
def main():
if os.geteuid() != 0:
raise RuntimeError('必须使用 root 执行证书同步')
# 先验证信任链、服务域名、有效期和密钥匹配,再修改运行文件。
for domain in DOMAINS:
run('openssl', 'verify', '-CAfile', '/etc/ssl/certs/ca-certificates.crt',
'-untrusted', str(LINEAGE / 'solit.cn.issuer.crt'), '-verify_hostname', domain,
str(LINEAGE / 'solit.cn.crt'))
run('openssl', 'x509', '-in', str(LINEAGE / 'solit.cn.crt'), '-checkend', '604800', '-noout')
public_cert = run('openssl', 'x509', '-in', str(LINEAGE / 'solit.cn.crt'), '-pubkey', '-noout')
public_key = run('openssl', 'pkey', '-in', str(LINEAGE / 'solit.cn.key'), '-pubout')
if public_cert != public_key:
raise RuntimeError('证书与私钥不匹配')
if TARGET.is_symlink() or TARGET.resolve() != TARGET:
raise RuntimeError('证书目标目录不能包含符号链接')
pairs = [('solit.cn.key', 'custom.key'), ('solit.cn.crt', 'custom.crt')]
for _, name in pairs:
path = TARGET / name
if path.is_symlink() or not path.is_file() or path.stat().st_nlink != 1:
raise RuntimeError('现有证书文件类型不符合要求')
if all((LINEAGE / source).read_bytes() == (TARGET / name).read_bytes() for source, name in pairs):
print('证书校验通过,运行文件已同步')
return
identity = pwd.getpwnam('smail')
stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S') + '-' + uuid.uuid4().hex[:8]
backup = Path('/var/backups') / ('smail-certificate-' + stamp)
backup.mkdir(mode=0o700)
for _, name in pairs:
path = TARGET / name
if path.is_symlink() or not path.is_file():
raise RuntimeError('现有证书文件类型不符合要求')
shutil.copy2(path, backup / name)
os.chmod(backup / name, 0o600)
try:
for source, name in pairs:
replace_file(TARGET / name, (LINEAGE / source).read_bytes(), identity.pw_uid, identity.pw_gid)
run('nginx', '-t')
run('systemctl', 'reload', 'nginx')
except Exception:
for _, name in pairs:
replace_file(TARGET / name, (backup / name).read_bytes(), identity.pw_uid, identity.pw_gid)
raise
fingerprint = hashlib.sha256(run('openssl', 'x509', '-in', str(TARGET / 'custom.crt'), '-outform', 'DER')).hexdigest()
print('证书已同步,SHA256=' + fingerprint)
print('回退副本=' + str(backup))
if __name__ == '__main__':
main()