#!/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