feat(deploy): 补齐凭据加密接入迁移和密钥备份恢复
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
Test deployment public URL (Linux) / test-linux (pull_request) Canceled after 0s
Test start.ps1 (Windows) / test-windows (pull_request) Canceled after 0s

首次安装默认生成独立加密密钥,更新旧部署保留原配置,通过显式开关进入迁移窗口。增加生产迁移包装入口和完整密钥环备份恢复工具,并说明旧镜像升级、严格迁移、轮换及还原流程。

已通过 Bash 安全环境及密钥备份回归;对应 PowerShell 5.1 和 7 脚本已在 Windows 原生环境各通过 49 项验收。宁波服务器验收尚未执行。
This commit is contained in:
2026-10-12 22:24:16 +08:00
parent f59dd33097
commit 5cff1041a9
15 changed files with 563 additions and 0 deletions
+56
View File
@@ -115,6 +115,61 @@ ensure_initial_admin_password() {
echo " ✓ 已初始化管理员密码: CONFIG_INITIAL_ADMIN_PASSWORD"
}
init_credential_encryption_env() {
local file="$1" mode="$2" active ring normalized entry id encoded decoded reencoded ids='' legacy
active="$(read_env_value "$file" CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID)"
ring="$(read_env_value "$file" CONFIG_CREDENTIAL_ENCRYPTION_KEYRING)"
legacy="$(read_env_value "$file" CONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT)"
case "$legacy" in ''|true|false) ;; *) echo '❌ 旧凭据兼容开关只能为 true/false' >&2; return 1 ;; esac
if [ -z "$active" ] && [ -z "$ring" ]; then
if [ "$mode" != new ] && [ "$legacy" = false ]; then
echo '❌ 已关闭旧凭据兼容但缺少加密密钥,请恢复原配置' >&2
return 1
fi
# Existing sites opt in explicitly: never invent a replacement after key loss.
if [ "$mode" != new ] && [ "${DEPLOY_ENABLE_CREDENTIAL_ENCRYPTION:-false}" != true ]; then
if [ "$legacy" = false ]; then
echo '❌ 已关闭旧凭据兼容但缺少加密密钥,请恢复原配置' >&2
return 1
fi
echo ' ℹ 凭据加密尚未接入,保留旧部署兼容;首次接入使用 DEPLOY_ENABLE_CREDENTIAL_ENCRYPTION=true'
return 0
fi
command -v openssl >/dev/null 2>&1 || { echo '❌ 凭据加密初始化需要 openssl' >&2; return 1; }
active=v1
encoded="$(openssl rand -base64 32 | tr -d '\r\n')" || return 1
ring="{\"${active}\":\"${encoded}\"}"
write_env_value "$file" CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID "$active"
write_env_value "$file" CONFIG_CREDENTIAL_ENCRYPTION_KEYRING "'$ring'"
echo ' ✓ 已初始化独立凭据加密密钥(未输出密钥)'
fi
# Values have a restricted JSON grammar; no eval/source or secret command arguments.
normalized="$(printf '%s' "$ring" | tr -d ' \t\r\n')"
if ! [[ "$active" =~ ^[A-Za-z0-9_-]{1,64}$ ]] ||
! printf '%s' "$ring" | LC_ALL=C grep -Eq '^\{[[:space:]]*("[A-Za-z0-9_-]{1,64}"[[:space:]]*:[[:space:]]*"[A-Za-z0-9+/]{43}="[[:space:]]*,[[:space:]]*)*"[A-Za-z0-9_-]{1,64}"[[:space:]]*:[[:space:]]*"[A-Za-z0-9+/]{43}="[[:space:]]*\}$'; then
echo '❌ 凭据加密配置不完整或无效,请恢复原密钥;不会自动替换' >&2
return 1
fi
command -v openssl >/dev/null 2>&1 || { echo '❌ 凭据密钥校验需要 openssl' >&2; return 1; }
while IFS= read -r entry; do
id="${entry%%:*}"; id="${id#\"}"; id="${id%\"}"
encoded="${entry#*:}"; encoded="${encoded#\"}"; encoded="${encoded%\"}"
case ",$ids," in *",$id,"*) echo '❌ 凭据密钥 ID 重复' >&2; return 1 ;; esac
ids="${ids:+$ids,}$id"
decoded="$(printf '%s' "$encoded" | openssl base64 -d -A | wc -c | tr -d ' ')"
reencoded="$(printf '%s' "$encoded" | openssl base64 -d -A | openssl base64 -A)"
if [ "$decoded" != 32 ] || [ "$reencoded" != "$encoded" ]; then
echo '❌ 凭据密钥必须为标准 Base64 编码的 32 字节随机值' >&2; return 1
fi
done < <(printf '%s\n' "$normalized" | sed 's/^{//;s/}$//' | tr ',' '\n')
case ",$ids," in *",$active,"*) ;; *) echo '❌ 当前凭据密钥 ID 不在 keyring 中' >&2; return 1 ;; esac
if [ -z "$legacy" ]; then
if [ "$mode" = new ]; then legacy=false; else legacy=true; fi
write_env_value "$file" CONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT "$legacy"
fi
case "$legacy" in true|false) ;; *) echo '❌ 旧凭据兼容开关只能为 true/false' >&2; return 1 ;; esac
}
init_security_env() {
local file="${1:-.env}"
local mode="${2:-upgrade}"
@@ -127,6 +182,7 @@ init_security_env() {
return 1
fi
init_credential_encryption_env "$file" "$mode" || return 1
ensure_security_secret "$file" "CONFIG_JWT_SECRET" "this is a very secret secret"
ensure_security_secret "$file" "WS_AUTH_WS_TICKET_SECRET"
ensure_security_secret "$file" "SYSTEM_UPDATE_INTERNAL_TOKEN"