function New-SecuritySecret { $bytes = New-Object byte[] 48 $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() try { $rng.GetBytes($bytes) } finally { $rng.Dispose() } return [Convert]::ToBase64String($bytes) } function New-InitialAdminPassword { $bytes = New-Object byte[] 10 $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() try { $rng.GetBytes($bytes) } finally { $rng.Dispose() } return ([BitConverter]::ToString($bytes) -replace "-", "").ToLowerInvariant() } function Get-EnvValue { param([string]$Content, [string]$Key) $match = [regex]::Match($Content, "(?m)^$([regex]::Escape($Key))=(.*)$") if (-not $match.Success) { return "" } $value = $match.Groups[1].Value.Trim() if ($value.Length -ge 2) { $first = $value.Substring(0, 1) $last = $value.Substring($value.Length - 1, 1) if (($first -eq "'" -and $last -eq "'") -or ($first -eq '"' -and $last -eq '"')) { return $value.Substring(1, $value.Length - 2) } } return $value } function Set-EnvValue { param([string]$Content, [string]$Key, [string]$Value) $pattern = "(?m)^$([regex]::Escape($Key))=.*(?:\r?\n)?" $replacement = "$Key=$Value`r`n" if ([regex]::IsMatch($Content, $pattern)) { return [regex]::Replace($Content, $pattern, $replacement, 1) } if ($Content -and -not $Content.EndsWith("`n")) { $Content += "`r`n" } return $Content + $replacement } function Initialize-SecurityEnv { param( [Parameter(Mandatory = $true)][string]$Path, [ValidateSet("new", "upgrade")][string]$Mode = "upgrade" ) if (-not (Test-Path $Path)) { throw "Environment file not found: $Path" } $content = Get-Content $Path -Raw -Encoding UTF8 if ($null -eq $content) { $content = "" } $active = Get-EnvValue $content 'CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID' $ringText = Get-EnvValue $content 'CONFIG_CREDENTIAL_ENCRYPTION_KEYRING' $legacy = Get-EnvValue $content 'CONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT' if ($legacy -and $legacy -notin @('true','false')) { throw 'Credential legacy flag must be true/false' } if (-not $active -and -not $ringText) { if ($Mode -ne 'new' -and $legacy -eq 'false') { throw 'Credential encryption key missing; restore the original configuration' } if ($Mode -eq 'new' -or $env:DEPLOY_ENABLE_CREDENTIAL_ENCRYPTION -eq 'true') { $bytes = New-Object byte[] 32 $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() try { $rng.GetBytes($bytes) } finally { $rng.Dispose() } $active = 'v1' $ringText = '{"v1":"' + [Convert]::ToBase64String($bytes) + '"}' $content = Set-EnvValue $content 'CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID' $active $content = Set-EnvValue $content 'CONFIG_CREDENTIAL_ENCRYPTION_KEYRING' ("'" + $ringText + "'") Write-Host ' [OK] Initialized independent credential encryption key (value omitted)' } elseif ($legacy -eq 'false') { throw 'Credential encryption key missing; restore the original configuration' } else { Write-Host ' [INFO] Existing deployment retains legacy compatibility; opt in using DEPLOY_ENABLE_CREDENTIAL_ENCRYPTION=true' } } if ($active -or $ringText) { try { if ($active -notmatch '^[A-Za-z0-9_-]{1,64}$') { throw 'invalid' } $normalized = $ringText -replace '\s', '' if ($ringText -notmatch '^\{\s*("[A-Za-z0-9_-]{1,64}"\s*:\s*"[A-Za-z0-9+/]{43}="\s*,\s*)*"[A-Za-z0-9_-]{1,64}"\s*:\s*"[A-Za-z0-9+/]{43}="\s*\}$') { throw 'invalid' } $ids = New-Object 'System.Collections.Generic.Dictionary[string,bool]' ([System.StringComparer]::Ordinal) foreach ($match in [regex]::Matches($normalized, '"([A-Za-z0-9_-]+)":"([A-Za-z0-9+/]+=)"')) { $id = $match.Groups[1].Value; $encoded = $match.Groups[2].Value if ($ids.ContainsKey($id)) { throw 'duplicate' } $key = [Convert]::FromBase64String($encoded) if ($key.Length -ne 32 -or [Convert]::ToBase64String($key) -cne $encoded) { throw 'invalid' } $ids[$id] = $true } if (-not $ids.ContainsKey($active)) { throw 'invalid' } } catch { throw 'Credential keyring invalid or incomplete; restore original keys (values omitted)' } if (-not $legacy) { if ($Mode -eq 'new') { $legacy = 'false' } else { $legacy = 'true' } $content = Set-EnvValue $content 'CONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT' $legacy } if ($legacy -notin @('true', 'false')) { throw 'Credential legacy flag must be true/false' } } $definitions = @( @{ Key = "CONFIG_JWT_SECRET"; Legacy = "this is a very secret secret"; MinLength = 32 }, @{ Key = "WS_AUTH_WS_TICKET_SECRET"; Legacy = ""; MinLength = 32 }, @{ Key = "SYSTEM_UPDATE_INTERNAL_TOKEN"; Legacy = ""; MinLength = 32 }, @{ Key = "WATCHTOWER_API_KEY"; Legacy = ""; MinLength = 32 }, @{ Key = "WATCHTOWER_HTTP_API_EVENTS_TOKEN"; Legacy = ""; MinLength = 32 }, @{ Key = "MODEL_CONVERSION_WORKER_TOKEN"; Legacy = ""; MinLength = 32 } ) if ($Mode -eq "new") { $definitions += @( @{ Key = "CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY"; Legacy = ""; MinLength = 32 }, @{ Key = "CONFIG_OTP_HASH_SECRET"; Legacy = ""; MinLength = 32 }, @{ Key = "CONFIG_AUDIT_HASH_PEPPER"; Legacy = ""; MinLength = 32 }, @{ Key = "CONFIG_AUDIT_INTEGRITY_KEY"; Legacy = ""; MinLength = 32 } ) } foreach ($definition in $definitions) { $current = Get-EnvValue $content $definition.Key $needsInitialization = $current.Length -lt $definition.MinLength -or ($definition.Legacy -and $current -eq $definition.Legacy) if (-not $needsInitialization) { continue } $content = Set-EnvValue $content $definition.Key (New-SecuritySecret) 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) { $content = Set-EnvValue $content "CONFIG_INITIAL_ADMIN_PASSWORD" (New-InitialAdminPassword) Write-Host " [OK] Initialized admin password: CONFIG_INITIAL_ADMIN_PASSWORD" -ForegroundColor Green } } [System.IO.File]::WriteAllText( (Resolve-Path $Path), $content, [System.Text.UTF8Encoding]::new($false) ) if ($env:OS -eq 'Windows_NT') { $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().User $acl = New-Object System.Security.AccessControl.FileSecurity $acl.SetAccessRuleProtection($true, $false) $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, 'FullControl', 'Allow') $acl.AddAccessRule($rule) Set-Acl -LiteralPath $Path -AclObject $acl } }