diff --git a/config/secrets.env.example b/config/secrets.env.example index 1f75d32..5d2266e 100644 --- a/config/secrets.env.example +++ b/config/secrets.env.example @@ -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 邮件密码(发送验证码邮件) # ============================================================================= diff --git a/scripts/deploy.sh b/scripts/deploy.sh index ab436fd..4e682c0 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -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" </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" diff --git a/scripts/reset.sh b/scripts/reset.sh index 5cd1d69..429f1f2 100644 --- a/scripts/reset.sh +++ b/scripts/reset.sh @@ -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:-(未设置)}" diff --git a/scripts/update.sh b/scripts/update.sh index f59f34a..bbec1b8 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -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" diff --git a/scripts/verify.sh b/scripts/verify.sh index 2c2680d..8d6e1c5 100644 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -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