Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c3d273ee3 |
+10
-2
@@ -12,7 +12,7 @@ release=${2:?}
|
||||
# Parse a small allowlist of non-secret release fields; never source a credential file.
|
||||
while IFS='=' read -r name value; do
|
||||
case "$name" in
|
||||
CHAT_IMAGE|CHAT_IMAGE_ID|CHAT_CERTBOT_IMAGE|CHAT_CERTBOT_IMAGE_ID|SOURCE_COMMIT|HOST_PORT|CHAT_ROOT|CHAT_RELEASE|CHAT_DATA_DIR|CHAT_LOG_DIR|CHAT_RUNTIME_ENV|CHAT_DOMAIN|CHAT_PROXY_DIR|CHAT_TLS_CERT|CHAT_TLS_KEY) ;;
|
||||
CHAT_IMAGE|CHAT_IMAGE_ID|CHAT_CERTBOT_IMAGE|CHAT_CERTBOT_IMAGE_ID|SOURCE_COMMIT|HOST_PORT|CHAT_ROOT|CHAT_RELEASE|CHAT_DATA_DIR|CHAT_LOG_DIR|CHAT_RUNTIME_ENV|CHAT_DOMAIN|CHAT_PROXY_DIR|CHAT_TLS_CERT|CHAT_TLS_KEY|CHAT_AUTH_DIR) ;;
|
||||
*) echo 'Unexpected release setting.' >&2; exit 2 ;;
|
||||
esac
|
||||
[[ "$value" =~ ^[A-Za-z0-9_./:-]+$ ]] || exit 2
|
||||
@@ -43,7 +43,8 @@ if [[ -n "$(ss -H -ltn "sport = :$HOST_PORT")" && "$old_port" != "127.0.0.1:$HOS
|
||||
fi
|
||||
|
||||
site="$CHAT_PROXY_DIR/baiyue-logistics-chat-gray.conf"
|
||||
auth="$CHAT_PROXY_DIR/.baiyue-logistics-chat.htpasswd"
|
||||
[[ "$CHAT_AUTH_DIR" == /*/baiyue-logistics-chat-gray-proxy && "$CHAT_AUTH_DIR" != *..* ]] || exit 2
|
||||
auth="$CHAT_AUTH_DIR/access.htpasswd"
|
||||
if [[ -f "$site" ]] && ! head -n 1 "$site" | grep -Fq 'Managed for baiyue-logistics-chat-gray'; then
|
||||
echo 'The proxy file is not owned by this application.' >&2
|
||||
exit 2
|
||||
@@ -55,6 +56,13 @@ for candidate in "$CHAT_PROXY_DIR/"*.conf; do
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
# The Nginx config directory may be root-only (for example BaoTa). Keep this
|
||||
# application's password hash in a separate worker-readable owned directory.
|
||||
if [[ -e "$CHAT_AUTH_DIR" ]]; then
|
||||
[[ -f "$CHAT_AUTH_DIR/.project" && "$(cat "$CHAT_AUTH_DIR/.project")" == baiyue-logistics-chat-gray ]] || exit 2
|
||||
fi
|
||||
install -d -m 755 "$CHAT_AUTH_DIR"
|
||||
printf 'baiyue-logistics-chat-gray\n' > "$CHAT_AUTH_DIR/.project"
|
||||
previous=''
|
||||
if [[ -f "$root/current-release" ]]; then
|
||||
previous=$(cat "$root/current-release")
|
||||
|
||||
@@ -29,6 +29,7 @@ class GrayConfig:
|
||||
image: str
|
||||
image_id: str
|
||||
certbot_image_id: str
|
||||
auth_dir: str = '/var/lib/baiyue-logistics-chat-gray-proxy'
|
||||
|
||||
@classmethod
|
||||
def from_environment(cls, environment):
|
||||
@@ -70,6 +71,7 @@ class GrayConfig:
|
||||
'CHAT_RUNTIME_ENV': self.release + '/runtime.env',
|
||||
'CHAT_DOMAIN': self.domain, 'CHAT_PROXY_DIR': self.proxy_dir,
|
||||
'CHAT_TLS_CERT': self.certificate, 'CHAT_TLS_KEY': self.private_key,
|
||||
'CHAT_AUTH_DIR': self.auth_dir,
|
||||
}
|
||||
return ''.join(f'{key}={value}\n' for key, value in fields.items())
|
||||
|
||||
@@ -91,7 +93,7 @@ server {{
|
||||
ssl_certificate_key {self.private_key};
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
auth_basic "Baiyue gray acceptance";
|
||||
auth_basic_user_file {self.proxy_dir}/.baiyue-logistics-chat.htpasswd;
|
||||
auth_basic_user_file {self.auth_dir}/access.htpasswd;
|
||||
client_max_body_size 12m;
|
||||
location / {{
|
||||
proxy_pass http://127.0.0.1:{self.port};
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
|
||||
模型密钥写入权限为 600 的 `runtime.env`,由 Compose 注入进程。验收密码只在 Jenkins 生成 Nginx 哈希,服务器保存哈希文件。
|
||||
|
||||
密码哈希位于独立的 `/var/lib/baiyue-logistics-chat-gray-proxy/access.htpasswd`,目录 755、文件 644,供 Nginx 工作进程读取;不放在可能只有 root 可访问的宝塔站点配置目录,也不放明文密码。该目录有本服务所有权标记,不复用未知目录。
|
||||
|
||||
更新前执行 SQLite 在线备份。应用启动或 Nginx 检查失败时,仅恢复本服务之前的 Compose 和站点配置,保留数据;不自动用旧库覆盖新记录。失败发布目录保留供定位,数据库降级及删除数据需另行处理。
|
||||
|
||||
新环境自动创建八张业务表,不创建默认客服账号或演示数据。验收用全站密码直接进入工作台;Nginx 验收账号不是应用内客服账号。
|
||||
|
||||
@@ -55,7 +55,8 @@ class GrayActivationTests(unittest.TestCase):
|
||||
directory = Path(temporary.name)
|
||||
root = directory / 'gray-logistics-chat'
|
||||
proxy = directory / 'proxy'
|
||||
for path in (root / 'data', root / 'logs', root / 'releases', proxy):
|
||||
auth = directory / 'baiyue-logistics-chat-gray-proxy'
|
||||
for path in (root / 'data', root / 'logs', root / 'releases', proxy, auth):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
(root / '.project').write_text('baiyue-logistics-chat-gray\n')
|
||||
(root / 'data/history.db').write_bytes(b'preserve-existing-data')
|
||||
@@ -69,12 +70,13 @@ class GrayActivationTests(unittest.TestCase):
|
||||
(root / 'current-release').write_text(str(previous) + '\n')
|
||||
old_site = '# Managed for baiyue-logistics-chat-gray\n# previous site\n'
|
||||
(proxy / 'baiyue-logistics-chat-gray.conf').write_text(old_site)
|
||||
(proxy / '.baiyue-logistics-chat.htpasswd').write_text('previous-auth-hash\n')
|
||||
(auth / '.project').write_text('another-service\n' if scenario == 'unowned-auth' else 'baiyue-logistics-chat-gray\n')
|
||||
(auth / 'access.htpasswd').write_text('previous-auth-hash\n')
|
||||
unrelated = proxy / 'unrelated.conf'
|
||||
unrelated.write_text('# unrelated site\n')
|
||||
config = GrayConfig('chat.example.com', 29013, str(root), str(root / 'releases/new'),
|
||||
str(proxy), str(certificate), str(private_key), 'a'*40,
|
||||
'baiyue-logistics-chat-gray:' + 'a'*12 + '-b7', 'sha256:' + 'd'*64, 'sha256:' + 'c'*64)
|
||||
'baiyue-logistics-chat-gray:' + 'a'*12 + '-b7', 'sha256:' + 'd'*64, 'sha256:' + 'c'*64, str(auth))
|
||||
release = Path(config.release)
|
||||
write_release(release, config, 'fixture-model-key')
|
||||
# Docker 26 and 29 may assign different IDs after an archive is loaded.
|
||||
@@ -103,13 +105,18 @@ class GrayActivationTests(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual((root / 'current-release').read_text().strip(), str(release))
|
||||
self.assertIn('server_name chat.example.com', (proxy / 'baiyue-logistics-chat-gray.conf').read_text())
|
||||
auth = proxy.parent / 'baiyue-logistics-chat-gray-proxy'
|
||||
self.assertEqual((auth / 'access.htpasswd').read_text(), 'new-auth-hash\n')
|
||||
self.assertIn(str(auth / 'access.htpasswd'), (proxy / 'baiyue-logistics-chat-gray.conf').read_text())
|
||||
self.assertEqual(auth.stat().st_mode & 0o777, 0o755)
|
||||
self.assertEqual((auth / 'access.htpasswd').stat().st_mode & 0o777, 0o644)
|
||||
|
||||
def assert_rolled_back(self, scenario):
|
||||
result, root, proxy, previous, release, old_site, calls = self.run_activation(scenario)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual((root / 'current-release').read_text().strip(), str(previous))
|
||||
self.assertEqual((proxy / 'baiyue-logistics-chat-gray.conf').read_text(), old_site)
|
||||
self.assertEqual((proxy / '.baiyue-logistics-chat.htpasswd').read_text(), 'previous-auth-hash\n')
|
||||
self.assertEqual((proxy.parent / 'baiyue-logistics-chat-gray-proxy/access.htpasswd').read_text(), 'previous-auth-hash\n')
|
||||
self.assertTrue(any(name == 'docker' and 'up' in args and str(previous / 'release.env') in args
|
||||
for name, args in calls), result.stderr)
|
||||
|
||||
@@ -132,6 +139,15 @@ class GrayActivationTests(unittest.TestCase):
|
||||
self.assertEqual((proxy / 'baiyue-logistics-chat-gray.conf').read_text(), old_site)
|
||||
self.assertFalse(any(name == 'docker' and 'compose' in args for name, args in calls))
|
||||
|
||||
def test_password_directory_owned_by_another_service_is_not_modified(self):
|
||||
result, root, proxy, previous, release, old_site, calls = self.run_activation('unowned-auth')
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
auth = proxy.parent / 'baiyue-logistics-chat-gray-proxy'
|
||||
self.assertEqual((auth / '.project').read_text(), 'another-service\n')
|
||||
self.assertEqual((auth / 'access.htpasswd').read_text(), 'previous-auth-hash\n')
|
||||
self.assertEqual((root / 'current-release').read_text().strip(), str(previous))
|
||||
self.assertFalse(any(name == 'docker' and 'compose' in args for name, args in calls))
|
||||
|
||||
def test_invalid_nginx_configuration_restores_previous_site_and_application(self):
|
||||
self.assert_rolled_back('proxy-fail')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user