forked from wangbo/easyai
首次安装默认生成独立加密密钥,更新旧部署保留原配置,通过显式开关进入迁移窗口。增加生产迁移包装入口和完整密钥环备份恢复工具,并说明旧镜像升级、严格迁移、轮换及还原流程。 已通过 Bash 安全环境及密钥备份回归;对应 PowerShell 5.1 和 7 脚本已在 Windows 原生环境各通过 49 项验收。宁波服务器验收尚未执行。
206 lines
8.3 KiB
Bash
206 lines
8.3 KiB
Bash
#!/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
|