Files
easyai/scripts/init-security-env.sh
T
easyai03 5cff1041a9 feat(deploy): 补齐凭据加密接入迁移和密钥备份恢复
首次安装默认生成独立加密密钥,更新旧部署保留原配置,通过显式开关进入迁移窗口。增加生产迁移包装入口和完整密钥环备份恢复工具,并说明旧镜像升级、严格迁移、轮换及还原流程。

已通过 Bash 安全环境及密钥备份回归;对应 PowerShell 5.1 和 7 脚本已在 Windows 原生环境各通过 49 项验收。宁波服务器验收尚未执行。
2026-10-12 22:24:16 +08:00

206 lines
8.3 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# 为本地 .env 初始化持久化安全密钥。只处理空值和已知历史默认值,
# 已存在的自定义值保持不变;数据库中的 JWT 轮转配置仍由后端优先使用。
generate_security_secret() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 48 | tr -d '\r\n'
return
fi
if [ -r /dev/urandom ]; then
od -An -N48 -tx1 /dev/urandom | tr -d ' \r\n'
return
fi
echo "❌ 无法生成安全随机值:需要 openssl 或 /dev/urandom" >&2
return 1
}
generate_initial_admin_password() {
if command -v openssl >/dev/null 2>&1; then
# 10 random bytes encoded as hexadecimal: exactly 20 alphanumeric characters.
openssl rand -hex 10 | tr -d '\r\n'
return
fi
if [ -r /dev/urandom ]; then
od -An -N10 -tx1 /dev/urandom | tr -d ' \r\n'
return
fi
echo "❌ 无法生成初始管理员密码:需要 openssl 或 /dev/urandom" >&2
return 1
}
read_env_value() {
local file="$1"
local key="$2"
local value
value="$(awk -v key="$key" 'index($0, key "=") == 1 { print substr($0, length(key) + 2); exit }' "$file")"
value="$(printf '%s' "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
case "$value" in
\"*\") value="${value#\"}"; value="${value%\"}" ;;
\'*\') value="${value#\'}"; value="${value%\'}" ;;
esac
printf '%s' "$value"
}
write_env_value() {
local file="$1"
local key="$2"
local value="$3"
local tmp
tmp="$(mktemp "${file}.tmp.XXXXXX")"
awk -v key="$key" -v value="$value" '
BEGIN { replaced = 0 }
index($0, key "=") == 1 {
if (!replaced) {
print key "=" value
replaced = 1
}
next
}
{ print }
END {
if (!replaced) print key "=" value
}
' "$file" > "$tmp"
chmod 600 "$tmp"
mv "$tmp" "$file"
}
ensure_security_secret() {
local file="$1"
local key="$2"
local legacy_value="${3:-}"
local minimum_length="${4:-32}"
local current
current="$(read_env_value "$file" "$key")"
if [ "${#current}" -ge "$minimum_length" ] && { [ -z "$legacy_value" ] || [ "$current" != "$legacy_value" ]; }; then
return 0
fi
write_env_value "$file" "$key" "$(generate_security_secret)"
echo " ✓ 已初始化安全密钥: ${key}"
}
ensure_env_list_item() {
local file="$1"
local key="$2"
local required_item="$3"
local default_value="${4:-}"
local current normalized
current="$(read_env_value "$file" "$key")"
if [ -z "$current" ]; then
current="$default_value"
fi
normalized="$(printf '%s' "$current" | tr -d '[:space:]')"
case ",${normalized}," in
*",${required_item},"*) return 0 ;;
esac
if [ -n "$current" ]; then
current="${current},${required_item}"
else
current="$required_item"
fi
write_env_value "$file" "$key" "$current"
echo " ✓ 已补充安全配置项: ${key}"
}
ensure_initial_admin_password() {
local file="$1"
local current
current="$(read_env_value "$file" "CONFIG_INITIAL_ADMIN_PASSWORD")"
if [ "${#current}" -ge 12 ]; then
return 0
fi
write_env_value "$file" "CONFIG_INITIAL_ADMIN_PASSWORD" "$(generate_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}"
if [ ! -f "$file" ]; then
echo "❌ 环境配置文件不存在: $file" >&2
return 1
fi
if [ "$mode" != "new" ] && [ "$mode" != "upgrade" ]; then
echo "❌ 安全环境初始化模式无效: $mode(仅支持 new/upgrade)" >&2
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"
ensure_security_secret "$file" "WATCHTOWER_API_KEY"
ensure_security_secret "$file" "WATCHTOWER_HTTP_API_EVENTS_TOKEN"
ensure_security_secret "$file" "MODEL_CONVERSION_WORKER_TOKEN"
ensure_env_list_item "$file" "WS_AUTH_METHODS" "ws_ticket" "none,bearer"
if [ "$mode" = "new" ]; then
ensure_security_secret "$file" "CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY"
ensure_security_secret "$file" "CONFIG_OTP_HASH_SECRET"
ensure_security_secret "$file" "CONFIG_AUDIT_HASH_PEPPER"
ensure_security_secret "$file" "CONFIG_AUDIT_INTEGRITY_KEY"
ensure_initial_admin_password "$file"
fi
chmod 600 "$file"
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
init_security_env "${1:-.env}" "${2:-upgrade}"
fi