精确更新证书、Nginx、TLS、重定向和续期演练用例,统一使用 cdis.cn 与 web.cdis.cn。
This commit is contained in:
parent
3ba7252948
commit
a334085897
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user