fix(security): provision private config signing keys
这个提交包含在:
父节点
465910caa0
当前提交
ebbf8e0381
@ -69,6 +69,16 @@ SDK_INTERNAL_TOKEN=
|
|||||||
XUQM_STORE_SECRET_MASTER_KEY=
|
XUQM_STORE_SECRET_MASTER_KEY=
|
||||||
XUQM_STORE_SECRET_KEY_VERSION=v1
|
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 邮件密码(发送验证码邮件)
|
# SMTP 邮件密码(发送验证码邮件)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@ -438,6 +438,14 @@ EOF
|
|||||||
chmod 600 "$ROOT_DIR/config/secrets.env"
|
chmod 600 "$ROOT_DIR/config/secrets.env"
|
||||||
ok "config/secrets.env 已写入 (chmod 600)"
|
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 — 业务服务容器内配置(全服务开启)
|
# config/xuqm.env — 业务服务容器内配置(全服务开启)
|
||||||
cat > "$ROOT_DIR/config/xuqm.env" <<EOF
|
cat > "$ROOT_DIR/config/xuqm.env" <<EOF
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@ -105,6 +105,51 @@ ensure_env_value() {
|
|||||||
fi
|
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() {
|
profile_contains() {
|
||||||
local profiles="$1"
|
local profiles="$1"
|
||||||
local target="$2"
|
local target="$2"
|
||||||
|
|||||||
@ -102,6 +102,13 @@ if ! grep -q "^XUQM_STORE_SECRET_MASTER_KEY=." "$ROOT_DIR/config/secrets.env" 2>
|
|||||||
ok "secrets.env 已生成应用商店监测加密主密钥"
|
ok "secrets.env 已生成应用商店监测加密主密钥"
|
||||||
fi
|
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 \
|
_CONSOLE_DOMAIN="$(grep '^CONSOLE_DOMAIN=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null \
|
||||||
| cut -d= -f2- | tr -d '"' | tr -d "'")"
|
| cut -d= -f2- | tr -d '"' | tr -d "'")"
|
||||||
ok "CONSOLE_DOMAIN=${_CONSOLE_DOMAIN:-(未设置)}"
|
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 已生成应用商店监测加密主密钥"
|
ok "secrets.env 已生成应用商店监测加密主密钥"
|
||||||
fi
|
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(一键更新功能需要此变量,旧版部署升级时自动写入)
|
# 迁移:补齐 PRIVATE_DEPLOY_ROOT(一键更新功能需要此变量,旧版部署升级时自动写入)
|
||||||
if ! grep -q "^PRIVATE_DEPLOY_ROOT=" "$ROOT_DIR/.env" 2>/dev/null; then
|
if ! grep -q "^PRIVATE_DEPLOY_ROOT=" "$ROOT_DIR/.env" 2>/dev/null; then
|
||||||
printf '\nPRIVATE_DEPLOY_ROOT=%s\n' "$ROOT_DIR" >> "$ROOT_DIR/.env"
|
printf '\nPRIVATE_DEPLOY_ROOT=%s\n' "$ROOT_DIR" >> "$ROOT_DIR/.env"
|
||||||
|
|||||||
@ -216,7 +216,7 @@ fi
|
|||||||
# Redis
|
# Redis
|
||||||
REDIS_CTR="$(container_id redis)"
|
REDIS_CTR="$(container_id redis)"
|
||||||
if [ -n "$REDIS_CTR" ]; then
|
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
|
if [ "$PING" = "PONG" ]; then
|
||||||
pass "Redis 响应 PONG"
|
pass "Redis 响应 PONG"
|
||||||
else
|
else
|
||||||
@ -233,7 +233,7 @@ section "3. 核心服务"
|
|||||||
|
|
||||||
# actuator/health
|
# actuator/health
|
||||||
HEALTH_BODY=$(http_body "$BASE_URL/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
|
if [ "$HEALTH_STATUS" = '"status":"UP"' ]; then
|
||||||
pass "tenant-service actuator/health = UP"
|
pass "tenant-service actuator/health = UP"
|
||||||
else
|
else
|
||||||
@ -313,7 +313,7 @@ section "5. 租户登录"
|
|||||||
|
|
||||||
# 获取验证码
|
# 获取验证码
|
||||||
CAPTCHA_RESP=$(http_body "$BASE_URL/api/auth/captcha")
|
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
|
LOGIN_OK=false
|
||||||
TOKEN=""
|
TOKEN=""
|
||||||
@ -324,7 +324,7 @@ if [ -n "$CAPTCHA_KEY" ]; then
|
|||||||
REDIS_CTR="$(container_id redis)"
|
REDIS_CTR="$(container_id redis)"
|
||||||
if [ -n "$REDIS_CTR" ]; then
|
if [ -n "$REDIS_CTR" ]; then
|
||||||
CAPTCHA_CODE=$(docker exec "$REDIS_CTR" redis-cli -a "${REDIS_PASSWORD:-}" --no-auth-warning \
|
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
|
if [ -n "$CAPTCHA_CODE" ]; then
|
||||||
# 尝试登录(用 bootstrap 邮箱)
|
# 尝试登录(用 bootstrap 邮箱)
|
||||||
@ -336,9 +336,9 @@ if [ -n "$CAPTCHA_KEY" ]; then
|
|||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d "{\"account\":\"${BOOTSTRAP_EMAIL}\",\"password\":\"${BOOTSTRAP_PASS}\",\"captchaKey\":\"${CAPTCHA_KEY}\",\"captchaCode\":\"${CAPTCHA_CODE}\"}" 2>/dev/null || true)
|
-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
|
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
|
LOGIN_OK=true
|
||||||
pass "租户登录成功"
|
pass "租户登录成功"
|
||||||
elif [ "$BOOTSTRAP_PASS" = "change-me-on-first-login" ] || [ -z "$BOOTSTRAP_PASS" ]; then
|
elif [ "$BOOTSTRAP_PASS" = "change-me-on-first-login" ] || [ -z "$BOOTSTRAP_PASS" ]; then
|
||||||
@ -400,7 +400,7 @@ APP_KEYS=""
|
|||||||
if [ -n "$MYSQL_CTR" ]; then
|
if [ -n "$MYSQL_CTR" ]; then
|
||||||
APP_KEYS=$(docker exec "$MYSQL_CTR" sh -c \
|
APP_KEYS=$(docker exec "$MYSQL_CTR" sh -c \
|
||||||
"mysql -u${MYSQL_USERNAME:-xuqm} -p${MYSQL_PASSWORD:-} ${MYSQL_DATABASE:-xuqm_private} -N \
|
"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
|
fi
|
||||||
|
|
||||||
if [ -z "$APP_KEYS" ] && [ -n "${DEFAULT_APP_KEY:-}" ]; then
|
if [ -z "$APP_KEYS" ] && [ -n "${DEFAULT_APP_KEY:-}" ]; then
|
||||||
@ -452,7 +452,7 @@ _IM_APP_KEY=""
|
|||||||
if [ -n "$MYSQL_CTR" ]; then
|
if [ -n "$MYSQL_CTR" ]; then
|
||||||
_IM_APP_KEY=$(docker exec "$MYSQL_CTR" sh -c \
|
_IM_APP_KEY=$(docker exec "$MYSQL_CTR" sh -c \
|
||||||
"mysql -u${MYSQL_USERNAME:-xuqm} -p${MYSQL_PASSWORD:-} ${MYSQL_DATABASE:-xuqm_private} -N \
|
"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
|
fi
|
||||||
[ -z "$_IM_APP_KEY" ] && _IM_APP_KEY="${APP_KEYS%% *}" # 降级到第一个 App Key
|
[ -z "$_IM_APP_KEY" ] && _IM_APP_KEY="${APP_KEYS%% *}" # 降级到第一个 App Key
|
||||||
|
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户