fix(security): provision private config signing keys

这个提交包含在:
徐勤民 2026-07-29 00:40:41 +08:00
父节点 465910caa0
当前提交 ebbf8e0381
共有 6 个文件被更改,包括 86 次插入8 次删除

查看文件

@ -69,6 +69,16 @@ SDK_INTERNAL_TOKEN=
XUQM_STORE_SECRET_MASTER_KEY=
XUQM_STORE_SECRET_KEY_VERSION=v1
# =============================================================================
# SDK 配置与插件发布清单签名密钥(首次安装自动生成,禁止手工轮换)
# =============================================================================
# Ed25519 PKCS#8 私钥、X.509 公钥及其稳定标识。三项必须作为同一组备份和恢复;
# 缺少任意一项时服务会拒绝启动,避免生成无法验证的新配置。
SDK_CONFIG_SIGNING_PRIVATE_KEY_BASE64=
SDK_CONFIG_SIGNING_PUBLIC_KEY_BASE64=
SDK_CONFIG_SIGNING_KEY_ID=
# =============================================================================
# SMTP 邮件密码(发送验证码邮件)
# =============================================================================

查看文件

@ -438,6 +438,14 @@ EOF
chmod 600 "$ROOT_DIR/config/secrets.env"
ok "config/secrets.env 已写入 (chmod 600)"
# 为 SDK 加密配置与插件发布清单生成部署独享 Ed25519 信任根。私钥只保存在
# secrets.env,安装输出和交付文档均不得展示其内容。
# shellcheck disable=SC1091
. "$ROOT_DIR/scripts/lib.sh"
ensure_sdk_config_signing_keypair "$ROOT_DIR/config/secrets.env" \
|| fail "SDK 配置签名密钥初始化失败"
ok "SDK 配置签名密钥已生成"
# config/xuqm.env — 业务服务容器内配置(全服务开启)
cat > "$ROOT_DIR/config/xuqm.env" <<EOF
# =============================================================================

查看文件

@ -105,6 +105,51 @@ ensure_env_value() {
fi
}
ensure_sdk_config_signing_keypair() {
local secrets_file="${1:-$ROOT_DIR/config/secrets.env}"
local private_key public_key key_id key_dir
if [ ! -f "$secrets_file" ]; then
mkdir -p "$(dirname "$secrets_file")"
cp "$ROOT_DIR/config/secrets.env.example" "$secrets_file"
chmod 600 "$secrets_file" || true
fi
private_key="$(grep '^SDK_CONFIG_SIGNING_PRIVATE_KEY_BASE64=' "$secrets_file" 2>/dev/null | cut -d= -f2- || true)"
public_key="$(grep '^SDK_CONFIG_SIGNING_PUBLIC_KEY_BASE64=' "$secrets_file" 2>/dev/null | cut -d= -f2- || true)"
key_id="$(grep '^SDK_CONFIG_SIGNING_KEY_ID=' "$secrets_file" 2>/dev/null | cut -d= -f2- || true)"
if [ -n "$private_key" ] && [ -n "$public_key" ] && [ -n "$key_id" ]; then
return 0
fi
if [ -n "$private_key" ] || [ -n "$public_key" ] || [ -n "$key_id" ]; then
printf 'SDK 配置签名密钥只配置了一部分,拒绝自动轮换;请恢复完整密钥对后重试\n' >&2
return 1
fi
command -v openssl >/dev/null 2>&1 || {
printf '生成 SDK 配置签名密钥需要 openssl\n' >&2
return 1
}
key_dir="$(mktemp -d "${TMPDIR:-/tmp}/xuqm-config-signing.XXXXXX")"
if ! openssl genpkey -algorithm Ed25519 -out "$key_dir/private.pem" >/dev/null 2>&1 ||
! openssl pkey -in "$key_dir/private.pem" -outform DER -out "$key_dir/private.der" >/dev/null 2>&1 ||
! openssl pkey -in "$key_dir/private.pem" -pubout -outform DER -out "$key_dir/public.der" >/dev/null 2>&1; then
rm -rf "$key_dir"
printf '生成 Ed25519 SDK 配置签名密钥失败\n' >&2
return 1
fi
private_key="$(base64 < "$key_dir/private.der" | tr -d '\r\n')"
public_key="$(base64 < "$key_dir/public.der" | tr -d '\r\n')"
key_id="private-$(openssl dgst -sha256 "$key_dir/public.der" | awk '{print substr($NF,1,16)}')"
rm -rf "$key_dir"
set_env_value "$secrets_file" "SDK_CONFIG_SIGNING_PRIVATE_KEY_BASE64" "$private_key"
set_env_value "$secrets_file" "SDK_CONFIG_SIGNING_PUBLIC_KEY_BASE64" "$public_key"
set_env_value "$secrets_file" "SDK_CONFIG_SIGNING_KEY_ID" "$key_id"
chmod 600 "$secrets_file" || true
}
profile_contains() {
local profiles="$1"
local target="$2"

查看文件

@ -102,6 +102,13 @@ if ! grep -q "^XUQM_STORE_SECRET_MASTER_KEY=." "$ROOT_DIR/config/secrets.env" 2>
ok "secrets.env 已生成应用商店监测加密主密钥"
fi
# reset 也可能是旧部署升级后的首次入口,必须补齐服务启动所需的签名信任根。
# shellcheck disable=SC1091
. "$ROOT_DIR/scripts/lib.sh"
ensure_sdk_config_signing_keypair "$ROOT_DIR/config/secrets.env" \
|| fail "SDK 配置签名密钥初始化失败"
ok "SDK 配置签名密钥已就绪"
_CONSOLE_DOMAIN="$(grep '^CONSOLE_DOMAIN=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null \
| cut -d= -f2- | tr -d '"' | tr -d "'")"
ok "CONSOLE_DOMAIN=${_CONSOLE_DOMAIN:-(未设置)}"

查看文件

@ -158,6 +158,14 @@ if ! grep -q "^XUQM_STORE_SECRET_MASTER_KEY=." "$ROOT_DIR/config/secrets.env" 2>
ok "secrets.env 已生成应用商店监测加密主密钥"
fi
# Config V2 和插件发布清单共用部署独享的 Ed25519 信任根。旧部署没有该密钥时
# 生成一次并永久保留;只存在半套配置时拒绝自动轮换,避免已签发配置突然失效。
# shellcheck disable=SC1091
. "$ROOT_DIR/scripts/lib.sh"
ensure_sdk_config_signing_keypair "$ROOT_DIR/config/secrets.env" \
|| fail "SDK 配置签名密钥初始化失败"
ok "SDK 配置签名密钥已就绪"
# 迁移:补齐 PRIVATE_DEPLOY_ROOT一键更新功能需要此变量,旧版部署升级时自动写入
if ! grep -q "^PRIVATE_DEPLOY_ROOT=" "$ROOT_DIR/.env" 2>/dev/null; then
printf '\nPRIVATE_DEPLOY_ROOT=%s\n' "$ROOT_DIR" >> "$ROOT_DIR/.env"

查看文件

@ -216,7 +216,7 @@ fi
# Redis
REDIS_CTR="$(container_id redis)"
if [ -n "$REDIS_CTR" ]; then
PING=$(docker exec "$REDIS_CTR" redis-cli -a "${REDIS_PASSWORD:-}" --no-auth-warning PING 2>/dev/null | tr -d '\r')
PING=$(docker exec "$REDIS_CTR" redis-cli -a "${REDIS_PASSWORD:-}" --no-auth-warning PING 2>/dev/null | tr -d '\r' || true)
if [ "$PING" = "PONG" ]; then
pass "Redis 响应 PONG"
else
@ -233,7 +233,7 @@ section "3. 核心服务"
# actuator/health
HEALTH_BODY=$(http_body "$BASE_URL/actuator/health")
HEALTH_STATUS=$(echo "$HEALTH_BODY" | grep -o '"status":"[^"]*"' | head -1)
HEALTH_STATUS=$(echo "$HEALTH_BODY" | grep -o '"status":"[^"]*"' | head -1 || true)
if [ "$HEALTH_STATUS" = '"status":"UP"' ]; then
pass "tenant-service actuator/health = UP"
else
@ -313,7 +313,7 @@ section "5. 租户登录"
# 获取验证码
CAPTCHA_RESP=$(http_body "$BASE_URL/api/auth/captcha")
CAPTCHA_KEY=$(echo "$CAPTCHA_RESP" | grep -o '"key":"[^"]*"' | head -1 | cut -d'"' -f4)
CAPTCHA_KEY=$(echo "$CAPTCHA_RESP" | grep -o '"key":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
LOGIN_OK=false
TOKEN=""
@ -324,7 +324,7 @@ if [ -n "$CAPTCHA_KEY" ]; then
REDIS_CTR="$(container_id redis)"
if [ -n "$REDIS_CTR" ]; then
CAPTCHA_CODE=$(docker exec "$REDIS_CTR" redis-cli -a "${REDIS_PASSWORD:-}" --no-auth-warning \
GET "captcha:${CAPTCHA_KEY}" 2>/dev/null | tr -d '\r')
GET "captcha:${CAPTCHA_KEY}" 2>/dev/null | tr -d '\r' || true)
if [ -n "$CAPTCHA_CODE" ]; then
# 尝试登录(用 bootstrap 邮箱)
@ -336,9 +336,9 @@ if [ -n "$CAPTCHA_KEY" ]; then
-H 'Content-Type: application/json' \
-d "{\"account\":\"${BOOTSTRAP_EMAIL}\",\"password\":\"${BOOTSTRAP_PASS}\",\"captchaKey\":\"${CAPTCHA_KEY}\",\"captchaCode\":\"${CAPTCHA_CODE}\"}" 2>/dev/null || true)
LOGIN_CODE=$(echo "$LOGIN_RESP" | grep -o '"code":[0-9]*' | head -1 | grep -o '[0-9]*')
LOGIN_CODE=$(echo "$LOGIN_RESP" | grep -o '"code":[0-9]*' | head -1 | grep -o '[0-9]*' || true)
if [ "$LOGIN_CODE" = "200" ]; then
TOKEN=$(echo "$LOGIN_RESP" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
TOKEN=$(echo "$LOGIN_RESP" | grep -o '"token":"[^"]*"' | cut -d'"' -f4 || true)
LOGIN_OK=true
pass "租户登录成功"
elif [ "$BOOTSTRAP_PASS" = "change-me-on-first-login" ] || [ -z "$BOOTSTRAP_PASS" ]; then
@ -400,7 +400,7 @@ APP_KEYS=""
if [ -n "$MYSQL_CTR" ]; then
APP_KEYS=$(docker exec "$MYSQL_CTR" sh -c \
"mysql -u${MYSQL_USERNAME:-xuqm} -p${MYSQL_PASSWORD:-} ${MYSQL_DATABASE:-xuqm_private} -N \
-e 'SELECT app_key FROM t_app LIMIT 5;'" 2>/dev/null | tr '\n' ' ')
-e 'SELECT app_key FROM t_app LIMIT 5;'" 2>/dev/null | tr '\n' ' ' || true)
fi
if [ -z "$APP_KEYS" ] && [ -n "${DEFAULT_APP_KEY:-}" ]; then
@ -452,7 +452,7 @@ _IM_APP_KEY=""
if [ -n "$MYSQL_CTR" ]; then
_IM_APP_KEY=$(docker exec "$MYSQL_CTR" sh -c \
"mysql -u${MYSQL_USERNAME:-xuqm} -p${MYSQL_PASSWORD:-} ${MYSQL_DATABASE:-xuqm_private} -N \
-e \"SELECT app_key FROM t_app WHERE is_default=1 LIMIT 1;\"" 2>/dev/null | tr -d '\r\n')
-e \"SELECT app_key FROM t_app WHERE is_default=1 LIMIT 1;\"" 2>/dev/null | tr -d '\r\n' || true)
fi
[ -z "$_IM_APP_KEY" ] && _IM_APP_KEY="${APP_KEYS%% *}" # 降级到第一个 App Key