移除部署域名和初始化账号默认值
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-01 13:52:13 +08:00
parent a2748e7664
commit 8b72f56ace
8 changed files with 59 additions and 17 deletions

View File

@ -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=

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -66,7 +66,7 @@ export function usePage() {
const waitDesc = ref(lang.wait_desc);
const adminSettings = reactive({
"account": "admin",
"account": "",
"password": "",
"password2": "",
"hadSeted": false

View File

@ -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()))

View File

@ -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):

View File

@ -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)