统一部署验收域名为 cdis.cn
Some checks are pending
Security Checks / security (push) Waiting to run

精确更新证书、Nginx、TLS、重定向和续期演练用例,统一使用 cdis.cn 与 web.cdis.cn。
This commit is contained in:
smail 2026-10-01 13:24:02 +08:00
parent 3ba7252948
commit a334085897
8 changed files with 49 additions and 49 deletions

View File

@ -117,8 +117,8 @@ def main():
parser.error('线上配置与已审查版本不符')
proposed = (evidence / 'smail.conf').read_bytes()
expected = previous.replace(
b'/var/lib/smail-acme-tls/certificates/solit.cn.crt', b'/opt/smail/ssl/custom.crt').replace(
b'/var/lib/smail-acme-tls/certificates/solit.cn.key', b'/opt/smail/ssl/custom.key')
b'/var/lib/smail-acme-tls/certificates/cdis.cn.crt', b'/opt/smail/ssl/custom.crt').replace(
b'/var/lib/smail-acme-tls/certificates/cdis.cn.key', b'/opt/smail/ssl/custom.key')
if proposed != expected or proposed == previous:
parser.error('候选配置必须且只能改变 Smail 证书读取路径')
run('nginx', '-t')
@ -158,21 +158,21 @@ def main():
try:
scripts = Scripts()
scripts.feed(request('mail.solit.cn', 443).decode('utf-8'))
scripts.feed(request('mail.cdis.cn', 443).decode('utf-8'))
if not scripts.paths:
raise RuntimeError('未找到用于既有连接测试的真实静态资源')
asset = args.asset_path or scripts.paths[0]
expected_body = request('mail.solit.cn', 443, asset)
expected_body = request('mail.cdis.cn', 443, asset)
if len(expected_body) < 65536:
raise RuntimeError('静态资源过小,无法进行本次下载连续性检查')
for host, port in (('mail.solit.cn', 443), ('web.apmmk.cn', 443)):
for host, port in (('mail.cdis.cn', 443), ('web.cdis.cn', 443)):
thread = threading.Thread(target=monitor, args=(host, port))
thread.start()
threads.append(thread)
time.sleep(2)
if len(report['samples']) < 4 or any(not item['ok'] for item in report['samples']):
raise RuntimeError('迁移前连续探测未通过')
secure, response = request('mail.solit.cn', 443, asset, slow=True)
secure, response = request('mail.cdis.cn', 443, asset, slow=True)
pending = (secure, response)
prefix = response.read(1024)
report['connectionOpenedBeforeReload'] = True

View File

@ -15,7 +15,7 @@ from unittest.mock import patch
SERVER = 'https://acme-staging-v02.api.letsencrypt.org/directory'
SOURCE = Path('/usr/local/libexec/smail-acme/renew_certificate.py')
LEGACY_SOURCE_SHA256 = '6d0abbe9466f8becd61b1cb3c750bfffb938d16c5a9bcae0b96d1451227d4216'
DOMAINS = {'solit.cn', 'mail.solit.cn', 'smtp.solit.cn', 'imap.solit.cn', 'pop.solit.cn'}
DOMAINS = {'cdis.cn', 'mail.cdis.cn', 'smtp.cdis.cn', 'imap.cdis.cn', 'pop.cdis.cn'}
RUN = subprocess.run
@ -59,14 +59,14 @@ def main():
run(['nginx', '-t'], check=True)
run(['systemctl', 'is-active', '--quiet', 'nginx'], check=True)
certificate = stage / 'certificates/solit.cn.crt'
key = stage / 'certificates/solit.cn.key'
certificate = stage / 'certificates/cdis.cn.crt'
key = stage / 'certificates/cdis.cn.key'
old_certificate = digest(certificate)
old_key = digest(key)
protected = [Path('/opt/smail/smail'), Path('/opt/smail/smail-admin'),
Path('/opt/smail/ssl/custom.crt'), Path('/opt/smail/ssl/custom.key'),
Path('/var/lib/smail-acme-tls/certificates/solit.cn.crt'),
Path('/var/lib/smail-acme-tls/certificates/solit.cn.key')]
Path('/var/lib/smail-acme-tls/certificates/cdis.cn.crt'),
Path('/var/lib/smail-acme-tls/certificates/cdis.cn.key')]
protected.extend(sorted(Path('/etc/nginx/conf.d').glob('*.conf')))
before = {path: digest(path) for path in protected}
spec = importlib.util.spec_from_file_location('production_renew', SOURCE)

View File

@ -161,7 +161,7 @@ class DeploymentTests(unittest.TestCase):
'-subj', '/CN=Smail isolated test CA',
'-addext', 'keyUsage=critical,keyCertSign,cRLSign')
cls.real_run('openssl', 'req', '-new', '-newkey', 'rsa:2048', '-nodes',
'-keyout', str(cls.key), '-out', str(cls.csr), '-subj', '/CN=solit.cn',
'-keyout', str(cls.key), '-out', str(cls.csr), '-subj', '/CN=cdis.cn',
'-addext', 'basicConstraints=critical,CA:FALSE',
'-addext', 'keyUsage=critical,digitalSignature,keyEncipherment',
'-addext', 'extendedKeyUsage=serverAuth',
@ -180,9 +180,9 @@ class DeploymentTests(unittest.TestCase):
self.backups = self.directory / 'backups'
for directory in (self.source, self.target, self.backups):
directory.mkdir(mode=0o700)
for path, value in ((self.source / 'solit.cn.crt', self.new.read_bytes()),
(self.source / 'solit.cn.key', self.key.read_bytes()),
(self.source / 'solit.cn.issuer.crt', self.ca.read_bytes()),
for path, value in ((self.source / 'cdis.cn.crt', self.new.read_bytes()),
(self.source / 'cdis.cn.key', self.key.read_bytes()),
(self.source / 'cdis.cn.issuer.crt', self.ca.read_bytes()),
(self.target / 'custom.crt', self.old.read_bytes()),
(self.target / 'custom.key', self.key.read_bytes())):
path.write_bytes(value)
@ -235,20 +235,20 @@ class DeploymentTests(unittest.TestCase):
self.assertEqual(path.stat().st_mode & 0o077, 0)
def test_unchanged_certificate_checks_live_without_reload(self):
(self.source / 'solit.cn.crt').write_bytes(self.old.read_bytes())
(self.source / 'cdis.cn.crt').write_bytes(self.old.read_bytes())
deploy.main()
self.assertNotIn(('systemctl', 'reload', 'nginx'), self.calls)
self.live.assert_called_once()
def test_interrupted_publish_is_reconciled_when_files_already_match(self):
(self.source / 'solit.cn.crt').write_bytes(self.old.read_bytes())
(self.source / 'cdis.cn.crt').write_bytes(self.old.read_bytes())
self.live.side_effect = [RuntimeError('上次重载前中断'), None]
deploy.main()
self.assertEqual(self.calls.count(('systemctl', 'reload', 'nginx')), 1)
self.assertEqual(self.live.call_count, 2)
def test_failed_reconciliation_does_not_report_success(self):
(self.source / 'solit.cn.crt').write_bytes(self.old.read_bytes())
(self.source / 'cdis.cn.crt').write_bytes(self.old.read_bytes())
self.live.side_effect = RuntimeError('线上证书不匹配')
with self.assertRaisesRegex(RuntimeError, '线上证书不匹配'):
deploy.main()
@ -271,13 +271,13 @@ class DeploymentTests(unittest.TestCase):
def test_short_lived_certificate_is_rejected(self):
self.real_run('openssl', 'x509', '-req', '-in', str(self.csr), '-CA', str(self.ca),
'-CAkey', str(self.ca_key), '-set_serial', '3', '-days', '1',
'-copy_extensions', 'copy', '-out', str(self.source / 'solit.cn.crt'))
'-copy_extensions', 'copy', '-out', str(self.source / 'cdis.cn.crt'))
with self.assertRaises(subprocess.CalledProcessError):
deploy.main()
self.assert_old_untouched()
def test_mismatched_key_is_rejected(self):
(self.source / 'solit.cn.key').write_bytes(self.ca_key.read_bytes())
(self.source / 'cdis.cn.key').write_bytes(self.ca_key.read_bytes())
with self.assertRaisesRegex(RuntimeError, '不匹配'):
deploy.main()
self.assert_old_untouched()
@ -289,7 +289,7 @@ class DeploymentTests(unittest.TestCase):
self.assertEqual((self.target / 'custom.crt').read_bytes(), self.old.read_bytes())
def test_nginx_still_using_candidate_is_rejected(self):
self.config += 'ssl_certificate ' + str(self.source / 'solit.cn.crt') + ';\n'
self.config += 'ssl_certificate ' + str(self.source / 'cdis.cn.crt') + ';\n'
with self.assertRaisesRegex(RuntimeError, '签发目录'):
deploy.main()
self.assert_old_untouched()
@ -325,7 +325,7 @@ class DeploymentTests(unittest.TestCase):
self.live.assert_called_with(deploy.fingerprint(self.old))
def test_unreadable_private_permissions_are_rejected(self):
(self.source / 'solit.cn.key').chmod(0o644)
(self.source / 'cdis.cn.key').chmod(0o644)
with self.assertRaisesRegex(RuntimeError, '权限'):
deploy.main()
self.assert_old_untouched()
@ -353,7 +353,7 @@ class DeploymentTests(unittest.TestCase):
validate = deploy.validate
def mutate(snapshot):
(self.source / 'solit.cn.crt').write_bytes(b'changed-after-snapshot')
(self.source / 'cdis.cn.crt').write_bytes(b'changed-after-snapshot')
validate(snapshot)
with patch.object(deploy, 'validate', side_effect=mutate):
@ -372,7 +372,7 @@ class DeploymentTests(unittest.TestCase):
for name in ('client_body', 'proxy', 'fastcgi', 'uwsgi', 'scgi'))
+ 'server {\n'
+ 'listen 127.0.0.1:18443 ssl;\nlisten 127.0.0.1:18002 ssl;\n'
+ 'server_name mail.solit.cn;\n' + self.config
+ 'server_name mail.cdis.cn;\n' + self.config
+ 'location / { return 200 "ready"; }\n}\n}\n', encoding='utf-8')
arguments = ('nginx', '-c', str(configuration), '-p', str(self.directory))
@ -402,7 +402,7 @@ class DeploymentTests(unittest.TestCase):
deploy.main()
self.real_verify(deploy.fingerprint(self.new))
self.assertIsNone(process.poll())
(self.source / 'solit.cn.crt').write_bytes(self.old.read_bytes())
(self.source / 'cdis.cn.crt').write_bytes(self.old.read_bytes())
attempted = False
def fail_after_live_update(expected):

View File

@ -26,7 +26,7 @@ class WebrootTests(unittest.TestCase):
path.chmod(0o755)
for context in (patch.object(renew, 'WEBROOT', self.webroot),
patch.object(renew, 'ROOT', self.directory),
patch.object(renew, 'DOMAINS', ('solit.cn', 'web.solit.cn')),
patch.object(renew, 'DOMAINS', ('cdis.cn', 'web.cdis.cn')),
patch.dict(os.environ, {'SMAIL_ACME_CHALLENGE': 'http'}, clear=True)):
context.start()
self.addCleanup(context.stop)
@ -70,7 +70,7 @@ class WebrootTests(unittest.TestCase):
self.assertEqual(renew.challenge_arguments(), ['--http', '--http.webroot', str(self.webroot)])
def test_wildcard_certificate_rejects_http_challenge(self):
with patch.object(renew, 'DOMAINS', ('solit.cn', '*.solit.cn')):
with patch.object(renew, 'DOMAINS', ('cdis.cn', '*.cdis.cn')):
with self.assertRaisesRegex(RuntimeError, '泛域名证书必须使用 DNS 验证'):
renew.challenge_arguments()

View File

@ -12,15 +12,15 @@ RENEWAL = (ROOT / 'deploy/nginx/renew_certificate.py').read_text(encoding='utf-8
class DomainAcceptanceTest(unittest.TestCase):
def test_web_domain_is_the_only_public_web_proxy(self):
self.assertIn('server_name web.solit.cn;', NGINX)
self.assertIn('proxy_ssl_name web.solit.cn;', NGINX)
self.assertIn('return 308 https://web.solit.cn$request_uri;', NGINX)
self.assertNotIn('return 308 https://mail.solit.cn$request_uri;', NGINX)
self.assertIn('server_name __SMAIL_WEB_DOMAIN__;', NGINX)
self.assertIn('proxy_ssl_name __SMAIL_WEB_DOMAIN__;', NGINX)
self.assertIn('return 308 https://__SMAIL_WEB_DOMAIN__$request_uri;', NGINX)
self.assertNotIn('return 308 https://__SMAIL_MAIL_DOMAIN__$request_uri;', NGINX)
def test_certificate_requests_root_and_wildcard(self):
self.assertIn("DOMAINS = ('solit.cn', '*.solit.cn')", CERTIFICATE)
self.assertIn("CERTIFICATE_SNI = 'web.solit.cn'", CERTIFICATE)
self.assertIn("'web.solit.cn'", CERTIFICATE)
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 = deploy.DOMAINS', RENEWAL)

View File

@ -11,7 +11,7 @@ import verify_nginx_tls as verify
class RedirectAcceptanceTest(unittest.TestCase):
expected = 'https://web.solit.cn/acceptance?x=1'
expected = 'https://web.cdis.cn/acceptance?x=1'
def check(self, first, second):
results = {'checks': []}
@ -29,12 +29,12 @@ class RedirectAcceptanceTest(unittest.TestCase):
self.assertEqual(self.check(self.response(), self.response()), [])
def test_internal_https_port_is_failure(self):
failures = self.check(self.response(), self.response('https://web.solit.cn:8002/acceptance?x=1'))
failures = self.check(self.response(), self.response('https://web.cdis.cn:8002/acceptance?x=1'))
self.assertEqual(len(failures), 1)
self.assertEqual(failures[0]['port'], 8001)
def test_public_port_does_not_skip_validation(self):
failures = self.check(self.response('http://web.solit.cn/acceptance?x=1'), self.response())
failures = self.check(self.response('http://web.cdis.cn/acceptance?x=1'), self.response())
self.assertEqual([item['port'] for item in failures], [80])
def test_missing_location_records_failure(self):
@ -46,7 +46,7 @@ class RedirectAcceptanceTest(unittest.TestCase):
self.assertEqual(failures[0]['actualStatus'], 200)
def test_path_and_query_must_be_preserved(self):
for location in ('https://web.solit.cn/', 'https://web.solit.cn/acceptance',
for location in ('https://web.cdis.cn/', 'https://web.cdis.cn/acceptance',
'https://other.invalid/acceptance?x=1'):
with self.subTest(location=location):
self.assertTrue(self.check(self.response(), self.response(location)))

View File

@ -11,7 +11,7 @@ import ssl
import subprocess
HOST = 'web.solit.cn'
HOST = 'web.cdis.cn'
CONTEXT = ssl.create_default_context()
@ -47,9 +47,9 @@ def main():
leaf = subprocess.run(['openssl', 'x509', '-in', '/opt/smail/ssl/custom.crt', '-outform', 'DER'],
check=True, capture_output=True).stdout
expected = hashlib.sha256(leaf).hexdigest()
for port, host in [(443, HOST), (443, 'solit.cn'), (443, 'mail.solit.cn'),
(8002, HOST), (465, 'smtp.solit.cn'),
(993, 'imap.solit.cn'), (995, 'pop.solit.cn')]:
for port, host in [(443, HOST), (443, 'cdis.cn'), (443, 'mail.cdis.cn'),
(8002, HOST), (465, 'smtp.cdis.cn'),
(993, 'imap.cdis.cn'), (995, 'pop.cdis.cn')]:
with socket.create_connection(('127.0.0.1', port), timeout=10) as raw:
with CONTEXT.wrap_socket(raw, server_hostname=host) as stream:
actual = hashlib.sha256(stream.getpeercert(binary_form=True)).hexdigest()
@ -59,7 +59,7 @@ def main():
for port in (25, 587):
with smtplib.SMTP(timeout=10) as client:
assert client.connect('127.0.0.1', port)[0] == 220
client._host = 'smtp.solit.cn'
client._host = 'smtp.cdis.cn'
assert client.ehlo('localhost')[0] == 250
assert not client.has_extn('auth'), 'plaintext AUTH advertised'
assert client.has_extn('starttls')
@ -87,7 +87,7 @@ def main():
raise AssertionError('unknown SNI accepted')
root = json.loads(Path('/opt/smail/config/config.json').read_text())
assert root['http']['port'] == 8001 and root['http']['httpsPort'] == 8002
assert root['security']['webAuthnRpOrigins'] == ['https://web.solit.cn']
assert root['security']['webAuthnRpOrigins'] == ['https://web.cdis.cn']
results['fingerprintSHA256'] = expected
results['status'] = 'failed' if results['failures'] else 'passed'
print(json.dumps(results, ensure_ascii=False, indent=2))

View File

@ -42,7 +42,7 @@ def main():
args = parser.parse_args()
if not args.confirm_staging_challenge or os.geteuid() != 0 or run('hostname') != 'ikun2':
parser.error('只允许在 47 服务器明确执行测试 CA 的全新 HTTP 挑战')
if '*.solit.cn' in renew.DOMAINS:
if '*.cdis.cn' in renew.DOMAINS:
parser.error('当前泛域名证书必须使用 DNS-01,不能执行 HTTP-01 演练')
if file_state(CONFIG)['sha256'] != CONFIG_SHA:
parser.error('Nginx 配置与已审查模板不符,停止演练')
@ -80,7 +80,7 @@ def main():
Path('/usr/local/libexec/smail-acme/deploy_certificate.py'),
Path('/etc/systemd/system/smail-certificate-renew.service')]
protected.extend(sorted(Path('/etc/nginx/conf.d').glob('*.conf')))
protected.extend(sorted(Path('/var/lib/smail-acme-tls/certificates').glob('solit.cn.*')))
protected.extend(sorted(Path('/var/lib/smail-acme-tls/certificates').glob('cdis.cn.*')))
before = {str(path): file_state(path) for path in protected}
before_services = service_state()
challenge_dir = renew.WEBROOT / '.well-known/acme-challenge'
@ -92,7 +92,7 @@ def main():
stop = threading.Event()
def sample():
for host in ('mail.solit.cn', 'web.apmmk.cn'):
for host in ('mail.cdis.cn', 'web.cdis.cn'):
value = {'host': host, 'timeUTC': datetime.now(timezone.utc).isoformat()}
try:
value['bytes'] = len(monitor.request(host, 443))
@ -135,8 +135,8 @@ def main():
result = subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, timeout=630)
report['exitCode'] = result.returncode
assert result.returncode == 0, '全新 HTTP 挑战失败,日志已保留'
certificate = state / 'certificates/solit.cn.crt'
key = state / 'certificates/solit.cn.key'
certificate = state / 'certificates/cdis.cn.crt'
key = state / 'certificates/cdis.cn.key'
public_cert = run('openssl', 'x509', '-in', str(certificate), '-pubkey', '-noout')
public_key = run('openssl', 'pkey', '-in', str(key), '-pubout')
assert public_cert == public_key