feat(deploy): 自动初始化画布协作认证密钥
This commit is contained in:
@@ -56,7 +56,8 @@ function Initialize-SecurityEnv {
|
||||
$content = Get-Content $Path -Raw -Encoding UTF8
|
||||
if ($null -eq $content) { $content = "" }
|
||||
$definitions = @(
|
||||
@{ Key = "CONFIG_JWT_SECRET"; Legacy = "this is a very secret secret"; MinLength = 32 }
|
||||
@{ Key = "CONFIG_JWT_SECRET"; Legacy = "this is a very secret secret"; MinLength = 32 },
|
||||
@{ Key = "WS_AUTH_WS_TICKET_SECRET"; Legacy = ""; MinLength = 32 }
|
||||
)
|
||||
if ($Mode -eq "new") {
|
||||
$definitions += @(
|
||||
@@ -76,6 +77,14 @@ function Initialize-SecurityEnv {
|
||||
Write-Host " [OK] Initialized security secret: $($definition.Key)" -ForegroundColor Green
|
||||
}
|
||||
|
||||
$authMethods = Get-EnvValue $content "WS_AUTH_METHODS"
|
||||
if ([string]::IsNullOrWhiteSpace($authMethods)) { $authMethods = "none,bearer" }
|
||||
$hasWsTicket = @($authMethods.Split(",") | ForEach-Object { $_.Trim() }) -contains "ws_ticket"
|
||||
if (-not $hasWsTicket) {
|
||||
$content = Set-EnvValue $content "WS_AUTH_METHODS" ($authMethods + ",ws_ticket")
|
||||
Write-Host " [OK] Added security config item: WS_AUTH_METHODS" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if ($Mode -eq "new") {
|
||||
$adminPassword = Get-EnvValue $content "CONFIG_INITIAL_ADMIN_PASSWORD"
|
||||
if ($adminPassword.Length -lt 12) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#Requires -Version 5.1
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. (Join-Path $scriptDir "Initialize-SecurityEnv.ps1")
|
||||
|
||||
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("easyai-security-env-" + [Guid]::NewGuid())
|
||||
[System.IO.Directory]::CreateDirectory($tempDir) | Out-Null
|
||||
|
||||
function Write-TestEnv {
|
||||
param([string]$Name, [string[]]$Lines)
|
||||
$path = Join-Path $tempDir $Name
|
||||
[System.IO.File]::WriteAllText($path, ($Lines -join "`r`n") + "`r`n", [System.Text.UTF8Encoding]::new($false))
|
||||
return $path
|
||||
}
|
||||
|
||||
function Assert-MinLength {
|
||||
param([string]$Content, [string]$Key, [int]$Minimum)
|
||||
$value = Get-EnvValue $Content $Key
|
||||
if ($value.Length -lt $Minimum) { throw "$Key must contain at least $Minimum characters" }
|
||||
}
|
||||
|
||||
function Assert-Value {
|
||||
param([string]$Content, [string]$Key, [string]$Expected)
|
||||
$actual = Get-EnvValue $Content $Key
|
||||
if ($actual -ne $Expected) { throw "$Key expected '$Expected', got '$actual'" }
|
||||
}
|
||||
|
||||
try {
|
||||
$upgrade = Write-TestEnv "upgrade.env" @(
|
||||
"CONFIG_JWT_SECRET='this is a very secret secret'",
|
||||
"CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY=",
|
||||
"WS_AUTH_WS_TICKET_SECRET=",
|
||||
"WS_AUTH_METHODS=none,bearer"
|
||||
)
|
||||
Initialize-SecurityEnv -Path $upgrade -Mode upgrade
|
||||
$upgradeContent = Get-Content $upgrade -Raw -Encoding UTF8
|
||||
Assert-MinLength $upgradeContent "CONFIG_JWT_SECRET" 32
|
||||
Assert-MinLength $upgradeContent "WS_AUTH_WS_TICKET_SECRET" 32
|
||||
Assert-Value $upgradeContent "CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY" ""
|
||||
Assert-Value $upgradeContent "WS_AUTH_METHODS" "none,bearer,ws_ticket"
|
||||
$upgradeBeforeRepeat = $upgradeContent
|
||||
Initialize-SecurityEnv -Path $upgrade -Mode upgrade
|
||||
$upgradeAfterRepeat = Get-Content $upgrade -Raw -Encoding UTF8
|
||||
if ($upgradeAfterRepeat -cne $upgradeBeforeRepeat) { throw "Repeated upgrade must preserve generated values byte-for-byte" }
|
||||
|
||||
$preservedSecret = "existing-ws-ticket-secret-that-is-long-enough"
|
||||
$preserved = Write-TestEnv "preserved.env" @(
|
||||
"CONFIG_JWT_SECRET=existing-jwt-secret-that-is-long-enough",
|
||||
"WS_AUTH_WS_TICKET_SECRET=$preservedSecret",
|
||||
"WS_AUTH_METHODS=bearer,ws_ticket"
|
||||
)
|
||||
Initialize-SecurityEnv -Path $preserved -Mode upgrade
|
||||
$preservedContent = Get-Content $preserved -Raw -Encoding UTF8
|
||||
Assert-Value $preservedContent "WS_AUTH_WS_TICKET_SECRET" $preservedSecret
|
||||
Assert-Value $preservedContent "WS_AUTH_METHODS" "bearer,ws_ticket"
|
||||
|
||||
$fresh = Write-TestEnv "new.env" @(
|
||||
"CONFIG_JWT_SECRET=",
|
||||
"CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY=",
|
||||
"CONFIG_OTP_HASH_SECRET=",
|
||||
"CONFIG_AUDIT_HASH_PEPPER=",
|
||||
"CONFIG_AUDIT_INTEGRITY_KEY=",
|
||||
"CONFIG_INITIAL_ADMIN_PASSWORD=",
|
||||
"WS_AUTH_WS_TICKET_SECRET=",
|
||||
"WS_AUTH_METHODS="
|
||||
)
|
||||
Initialize-SecurityEnv -Path $fresh -Mode new
|
||||
$freshContent = Get-Content $fresh -Raw -Encoding UTF8
|
||||
foreach ($key in @(
|
||||
"CONFIG_JWT_SECRET",
|
||||
"CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY",
|
||||
"CONFIG_OTP_HASH_SECRET",
|
||||
"CONFIG_AUDIT_HASH_PEPPER",
|
||||
"CONFIG_AUDIT_INTEGRITY_KEY",
|
||||
"WS_AUTH_WS_TICKET_SECRET"
|
||||
)) {
|
||||
Assert-MinLength $freshContent $key 32
|
||||
}
|
||||
Assert-MinLength $freshContent "CONFIG_INITIAL_ADMIN_PASSWORD" 12
|
||||
Assert-Value $freshContent "WS_AUTH_METHODS" "none,bearer,ws_ticket"
|
||||
|
||||
Write-Host "Security environment PowerShell tests passed" -ForegroundColor Green
|
||||
} finally {
|
||||
Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -81,6 +81,29 @@ ensure_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
|
||||
@@ -105,6 +128,8 @@ init_security_env() {
|
||||
fi
|
||||
|
||||
ensure_security_secret "$file" "CONFIG_JWT_SECRET" "this is a very secret secret"
|
||||
ensure_security_secret "$file" "WS_AUTH_WS_TICKET_SECRET"
|
||||
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"
|
||||
|
||||
@@ -77,6 +77,21 @@ assert server_ports[0].get("host_ip") == expected, server_ports
|
||||
' "$expected_server_host_ip"
|
||||
}
|
||||
|
||||
assert_canvas_ws_auth_config() {
|
||||
docker compose config --format json | python3 -c '
|
||||
import json, sys
|
||||
config = json.load(sys.stdin)
|
||||
server_env = config["services"]["easyai-server"].get("environment") or {}
|
||||
gateway_env = config["services"]["ws-gateway"].get("environment") or {}
|
||||
server_secret = str(server_env.get("WS_AUTH_WS_TICKET_SECRET") or "")
|
||||
gateway_secret = str(gateway_env.get("WS_AUTH_WS_TICKET_SECRET") or "")
|
||||
assert len(server_secret.encode()) >= 32, "server-main WS ticket secret is missing or too short"
|
||||
assert server_secret == gateway_secret, "server-main and ws-gateway WS ticket secrets differ"
|
||||
methods = {item.strip() for item in str(gateway_env.get("WS_AUTH_METHODS") or "").split(",") if item.strip()}
|
||||
assert "ws_ticket" in methods, f"ws-gateway does not advertise ws_ticket: {sorted(methods)}"
|
||||
'
|
||||
}
|
||||
|
||||
sed -i.bak 's/^SERVER_HTTP_PORT=.*/SERVER_HTTP_PORT=4100/' .env.sample
|
||||
rm -f .env.sample.bak
|
||||
DEPLOY_NON_INTERACTIVE=1 \
|
||||
@@ -89,6 +104,7 @@ grep -qx 'CONFIG_PUBLIC_API_BASE_URL=http://10.0.0.8:4100' .env
|
||||
grep -qx 'SERVER_HTTP_BIND_IP=0.0.0.0' .env
|
||||
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||
assert_compose_exposure 0.0.0.0
|
||||
assert_canvas_ws_auth_config
|
||||
fi
|
||||
|
||||
reset_case
|
||||
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=scripts/init-security-env.sh
|
||||
. "${SCRIPT_DIR}/init-security-env.sh"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
write_env() {
|
||||
local file="$1"
|
||||
shift
|
||||
printf '%s\n' "$@" > "$file"
|
||||
}
|
||||
|
||||
assert_min_length() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
local minimum="$3"
|
||||
local value
|
||||
value="$(read_env_value "$file" "$key")"
|
||||
[ "${#value}" -ge "$minimum" ] || fail "$key must contain at least $minimum characters"
|
||||
}
|
||||
|
||||
assert_value() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
local expected="$3"
|
||||
local actual
|
||||
actual="$(read_env_value "$file" "$key")"
|
||||
[ "$actual" = "$expected" ] || fail "$key expected '$expected', got '$actual'"
|
||||
}
|
||||
|
||||
assert_single_key() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
local count
|
||||
count="$(grep -c "^${key}=" "$file" || true)"
|
||||
[ "$count" -eq 1 ] || fail "$key must appear exactly once"
|
||||
}
|
||||
|
||||
file_sha256() {
|
||||
local file="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$file" | awk '{print $1}'
|
||||
else
|
||||
shasum -a 256 "$file" | awk '{print $1}'
|
||||
fi
|
||||
}
|
||||
|
||||
upgrade_env="$TMP_DIR/upgrade.env"
|
||||
upgrade_log="$TMP_DIR/upgrade.log"
|
||||
write_env "$upgrade_env" \
|
||||
"CONFIG_JWT_SECRET='this is a very secret secret'" \
|
||||
"CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY=" \
|
||||
"WS_AUTH_WS_TICKET_SECRET=" \
|
||||
"WS_AUTH_METHODS=none,bearer"
|
||||
init_security_env "$upgrade_env" upgrade > "$upgrade_log"
|
||||
|
||||
assert_min_length "$upgrade_env" "CONFIG_JWT_SECRET" 32
|
||||
assert_min_length "$upgrade_env" "WS_AUTH_WS_TICKET_SECRET" 32
|
||||
assert_value "$upgrade_env" "CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY" ""
|
||||
assert_value "$upgrade_env" "WS_AUTH_METHODS" "none,bearer,ws_ticket"
|
||||
assert_single_key "$upgrade_env" "WS_AUTH_WS_TICKET_SECRET"
|
||||
assert_single_key "$upgrade_env" "WS_AUTH_METHODS"
|
||||
|
||||
upgrade_jwt="$(read_env_value "$upgrade_env" "CONFIG_JWT_SECRET")"
|
||||
upgrade_ws="$(read_env_value "$upgrade_env" "WS_AUTH_WS_TICKET_SECRET")"
|
||||
if grep -Fq "$upgrade_jwt" "$upgrade_log" || grep -Fq "$upgrade_ws" "$upgrade_log"; then
|
||||
fail "security initialization output must not reveal generated secrets"
|
||||
fi
|
||||
|
||||
before_repeat="$(file_sha256 "$upgrade_env")"
|
||||
init_security_env "$upgrade_env" upgrade > "$TMP_DIR/repeat.log"
|
||||
after_repeat="$(file_sha256 "$upgrade_env")"
|
||||
[ "$before_repeat" = "$after_repeat" ] || fail "repeated upgrade must preserve generated values byte-for-byte"
|
||||
|
||||
preserved_secret="existing-ws-ticket-secret-that-is-long-enough"
|
||||
preserved_env="$TMP_DIR/preserved.env"
|
||||
write_env "$preserved_env" \
|
||||
"CONFIG_JWT_SECRET=existing-jwt-secret-that-is-long-enough" \
|
||||
"WS_AUTH_WS_TICKET_SECRET=$preserved_secret" \
|
||||
"WS_AUTH_METHODS=bearer,ws_ticket"
|
||||
init_security_env "$preserved_env" upgrade > "$TMP_DIR/preserved.log"
|
||||
assert_value "$preserved_env" "WS_AUTH_WS_TICKET_SECRET" "$preserved_secret"
|
||||
assert_value "$preserved_env" "WS_AUTH_METHODS" "bearer,ws_ticket"
|
||||
|
||||
new_env="$TMP_DIR/new.env"
|
||||
write_env "$new_env" \
|
||||
"CONFIG_JWT_SECRET=" \
|
||||
"CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY=" \
|
||||
"CONFIG_OTP_HASH_SECRET=" \
|
||||
"CONFIG_AUDIT_HASH_PEPPER=" \
|
||||
"CONFIG_AUDIT_INTEGRITY_KEY=" \
|
||||
"CONFIG_INITIAL_ADMIN_PASSWORD=" \
|
||||
"WS_AUTH_WS_TICKET_SECRET=" \
|
||||
"WS_AUTH_METHODS="
|
||||
init_security_env "$new_env" new > "$TMP_DIR/new.log"
|
||||
for key in \
|
||||
CONFIG_JWT_SECRET \
|
||||
CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY \
|
||||
CONFIG_OTP_HASH_SECRET \
|
||||
CONFIG_AUDIT_HASH_PEPPER \
|
||||
CONFIG_AUDIT_INTEGRITY_KEY \
|
||||
WS_AUTH_WS_TICKET_SECRET; do
|
||||
assert_min_length "$new_env" "$key" 32
|
||||
done
|
||||
assert_min_length "$new_env" "CONFIG_INITIAL_ADMIN_PASSWORD" 12
|
||||
assert_value "$new_env" "WS_AUTH_METHODS" "none,bearer,ws_ticket"
|
||||
|
||||
mode="$(stat -c '%a' "$new_env" 2>/dev/null || stat -f '%Lp' "$new_env")"
|
||||
[ "$mode" = "600" ] || fail "generated environment file mode must be 600, got $mode"
|
||||
|
||||
echo "Security environment shell tests passed"
|
||||
Reference in New Issue
Block a user