This commit is contained in:
parent
a2748e7664
commit
8b72f56ace
@ -1,6 +1,6 @@
|
||||
# 仅为阿里云 DNS 配置示例,实际服务商和授权范围必须先确认。
|
||||
# 正式文件位于 /etc/smail/acme-dns.env,归 root 所有,权限为 0600。
|
||||
# 禁止将填写后的文件、密钥或环境变量输出提交到仓库。
|
||||
SMAIL_ACME_DNS_PROVIDER=alidns
|
||||
SMAIL_ACME_DNS_PROVIDER=
|
||||
ALICLOUD_ACCESS_KEY=
|
||||
ALICLOUD_SECRET_KEY=
|
||||
|
||||
@ -2,4 +2,6 @@
|
||||
# 正式文件为 /etc/smail/acme-renew.env,root 所有,权限 0600。
|
||||
# HTTP 模式复用 Nginx 的 /var/lib/smail-acme,不监听新端口,不停止服务。
|
||||
# 使用 DNS 模式时设为 dns,并在 acme-dns.env 中配置专用服务商凭据。
|
||||
SMAIL_ACME_CHALLENGE=http
|
||||
SMAIL_MAIL_DOMAIN=
|
||||
SMAIL_WEB_DOMAIN=
|
||||
SMAIL_ACME_CHALLENGE=dns
|
||||
|
||||
@ -8,6 +8,7 @@ import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pwd
|
||||
import re
|
||||
import shlex
|
||||
import socket
|
||||
import ssl
|
||||
@ -17,10 +18,25 @@ import time
|
||||
import uuid
|
||||
|
||||
|
||||
DOMAINS = ('cdis.cn', '*.cdis.cn')
|
||||
VERIFY_HOSTNAMES = ('cdis.cn', 'web.cdis.cn', 'mail.cdis.cn', 'smtp.cdis.cn',
|
||||
'imap.cdis.cn', 'pop.cdis.cn')
|
||||
CERTIFICATE_SNI = 'web.cdis.cn'
|
||||
def configured_domain(name):
|
||||
value = os.environ[name].strip().lower()
|
||||
labels = value.split('.')
|
||||
if (len(value) > 253 or len(labels) < 2
|
||||
or any(not re.fullmatch(r'[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?', label)
|
||||
for label in labels)):
|
||||
raise ValueError(name)
|
||||
return value
|
||||
|
||||
|
||||
MAIL_DOMAIN = configured_domain('SMAIL_MAIL_DOMAIN')
|
||||
CERTIFICATE_SNI = configured_domain('SMAIL_WEB_DOMAIN')
|
||||
if (CERTIFICATE_SNI != MAIL_DOMAIN
|
||||
and (not CERTIFICATE_SNI.endswith('.' + MAIL_DOMAIN)
|
||||
or CERTIFICATE_SNI.count('.') != MAIL_DOMAIN.count('.') + 1)):
|
||||
raise ValueError('SMAIL_WEB_DOMAIN')
|
||||
DOMAINS = (MAIL_DOMAIN, '*.' + MAIL_DOMAIN)
|
||||
VERIFY_HOSTNAMES = (MAIL_DOMAIN, CERTIFICATE_SNI,
|
||||
*(prefix + '.' + MAIL_DOMAIN for prefix in ('mail', 'smtp', 'imap', 'pop')))
|
||||
LINEAGE = Path('/var/lib/smail-acme-tls/certificates')
|
||||
TARGET = Path('/opt/smail/ssl')
|
||||
BACKUPS = Path('/var/backups')
|
||||
@ -147,9 +163,9 @@ def publish():
|
||||
check_nginx_paths()
|
||||
run('systemctl', 'is-active', '--quiet', 'nginx')
|
||||
identity = pwd.getpwnam('smail')
|
||||
sources = {'candidate.crt': read_file(LINEAGE / 'cdis.cn.crt', 0),
|
||||
'candidate.key': read_file(LINEAGE / 'cdis.cn.key', 0),
|
||||
'candidate.issuer.crt': read_file(LINEAGE / 'cdis.cn.issuer.crt', 0),
|
||||
sources = {'candidate.crt': read_file(LINEAGE / (MAIL_DOMAIN + '.crt'), 0),
|
||||
'candidate.key': read_file(LINEAGE / (MAIL_DOMAIN + '.key'), 0),
|
||||
'candidate.issuer.crt': read_file(LINEAGE / (MAIL_DOMAIN + '.issuer.crt'), 0),
|
||||
'previous.crt': read_file(TARGET / 'custom.crt', identity.pw_uid),
|
||||
'previous.key': read_file(TARGET / 'custom.key', identity.pw_uid)}
|
||||
stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S') + '-' + uuid.uuid4().hex[:8]
|
||||
|
||||
@ -29,7 +29,7 @@ def challenge_arguments():
|
||||
return ['--dns', dns_provider()]
|
||||
if method != 'http':
|
||||
raise RuntimeError('验证方式必须为 dns 或 http,不允许自动回退到停机验证')
|
||||
if '*.cdis.cn' in DOMAINS:
|
||||
if any(domain.startswith('*.') for domain in DOMAINS):
|
||||
raise RuntimeError('泛域名证书必须使用 DNS 验证,不能使用 HTTP 验证')
|
||||
private = ROOT.lstat()
|
||||
if (ROOT.resolve(strict=True) != ROOT or not stat.S_ISDIR(private.st_mode)
|
||||
|
||||
@ -66,7 +66,7 @@ export function usePage() {
|
||||
const waitDesc = ref(lang.wait_desc);
|
||||
|
||||
const adminSettings = reactive({
|
||||
"account": "admin",
|
||||
"account": "",
|
||||
"password": "",
|
||||
"password2": "",
|
||||
"hadSeted": false
|
||||
|
||||
@ -17,11 +17,32 @@ SCRIPTS = BASE if (BASE / 'deploy_certificate.py').exists() else BASE.parents[1]
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
|
||||
import deploy_certificate as deploy
|
||||
import renew_certificate as renew
|
||||
with patch.dict(os.environ, {'SMAIL_MAIL_DOMAIN': 'cdis.cn', 'SMAIL_WEB_DOMAIN': 'web.cdis.cn'}):
|
||||
import deploy_certificate as deploy
|
||||
import renew_certificate as renew
|
||||
import nginx_certificate_cutover as cutover
|
||||
|
||||
|
||||
class DomainConfigurationTests(unittest.TestCase):
|
||||
def test_domain_requires_explicit_configuration(self):
|
||||
with patch.dict(os.environ, {}, clear=True), self.assertRaises(KeyError):
|
||||
deploy.configured_domain('SMAIL_MAIL_DOMAIN')
|
||||
|
||||
def test_domain_rejects_invalid_input(self):
|
||||
for value in ('', ' ', 'localhost', '../example.test', 'https://example.test',
|
||||
'*.example.test', 'example.test:443', 'example..test',
|
||||
'-mail.example.test', 'mail-.example.test',
|
||||
'a' * 64 + '.test', '.'.join(['a' * 63] * 4)):
|
||||
with self.subTest(value=value), \
|
||||
patch.dict(os.environ, {'SMAIL_MAIL_DOMAIN': value}), \
|
||||
self.assertRaises(ValueError):
|
||||
deploy.configured_domain('SMAIL_MAIL_DOMAIN')
|
||||
|
||||
def test_domain_uses_user_configuration(self):
|
||||
with patch.dict(os.environ, {'SMAIL_MAIL_DOMAIN': ' New.Example.Test '}):
|
||||
self.assertEqual(deploy.configured_domain('SMAIL_MAIL_DOMAIN'), 'new.example.test')
|
||||
|
||||
|
||||
class CutoverTests(unittest.TestCase):
|
||||
def test_atomic_config_preserves_mode_under_restrictive_umask(self):
|
||||
directory = Path(tempfile.mkdtemp(prefix='cutover-tests-', dir=Path.cwd()))
|
||||
|
||||
@ -13,7 +13,8 @@ from unittest.mock import patch
|
||||
BASE = Path(__file__).resolve().parent
|
||||
SCRIPTS = BASE if (BASE / 'renew_certificate.py').exists() else BASE.parents[1] / 'deploy/nginx'
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
import renew_certificate as renew
|
||||
with patch.dict(os.environ, {'SMAIL_MAIL_DOMAIN': 'cdis.cn', 'SMAIL_WEB_DOMAIN': 'web.cdis.cn'}):
|
||||
import renew_certificate as renew
|
||||
|
||||
|
||||
class WebrootTests(unittest.TestCase):
|
||||
|
||||
@ -18,9 +18,11 @@ class DomainAcceptanceTest(unittest.TestCase):
|
||||
self.assertNotIn('return 308 https://__SMAIL_MAIL_DOMAIN__$request_uri;', NGINX)
|
||||
|
||||
def test_certificate_requests_root_and_wildcard(self):
|
||||
self.assertIn("DOMAINS = ('cdis.cn', '*.cdis.cn')", CERTIFICATE)
|
||||
self.assertIn("CERTIFICATE_SNI = 'web.cdis.cn'", CERTIFICATE)
|
||||
self.assertIn("'web.cdis.cn'", CERTIFICATE)
|
||||
self.assertIn("DOMAINS = (MAIL_DOMAIN, '*.' + MAIL_DOMAIN)", CERTIFICATE)
|
||||
self.assertIn("configured_domain('SMAIL_MAIL_DOMAIN')", CERTIFICATE)
|
||||
self.assertIn("configured_domain('SMAIL_WEB_DOMAIN')", CERTIFICATE)
|
||||
self.assertNotIn('cdis.cn', CERTIFICATE)
|
||||
self.assertIn("any(domain.startswith('*.') for domain in DOMAINS)", RENEWAL)
|
||||
self.assertIn('DOMAINS = deploy.DOMAINS', RENEWAL)
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user