forked from wangbo/easyai
首次安装默认生成独立加密密钥,更新旧部署保留原配置,通过显式开关进入迁移窗口。增加生产迁移包装入口和完整密钥环备份恢复工具,并说明旧镜像升级、严格迁移、轮换及还原流程。 已通过 Bash 安全环境及密钥备份回归;对应 PowerShell 5.1 和 7 脚本已在 Windows 原生环境各通过 49 项验收。宁波服务器验收尚未执行。
69 lines
4.5 KiB
PowerShell
69 lines
4.5 KiB
PowerShell
#Requires -Version 5.1
|
|
param(
|
|
[Parameter(Mandatory=$true)][ValidateSet('export','restore')][string]$Mode,
|
|
[Parameter(Mandatory=$true)][string]$Path,
|
|
[string]$EnvPath = (Join-Path (Split-Path -Parent $PSScriptRoot) '.env')
|
|
)
|
|
$ErrorActionPreference = 'Stop'
|
|
. (Join-Path $PSScriptRoot 'Initialize-SecurityEnv.ps1')
|
|
function Protect-CredentialFile([string]$FilePath) {
|
|
if ($env:OS -eq 'Windows_NT') {
|
|
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
$acl = New-Object System.Security.AccessControl.FileSecurity
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', 'Allow')))
|
|
Set-Acl -LiteralPath $FilePath -AclObject $acl
|
|
} else { & chmod 600 $FilePath; if ($LASTEXITCODE) { throw 'Cannot protect credential file' } }
|
|
}
|
|
if (-not (Test-Path -LiteralPath $EnvPath -PathType Leaf)) { throw 'Prepare the deployment .env first' }
|
|
$EnvPath = [System.IO.Path]::GetFullPath($EnvPath)
|
|
$Path = [System.IO.Path]::GetFullPath($Path)
|
|
if ($EnvPath -eq $Path) { throw 'Keep the key backup separate from deployment configuration' }
|
|
$source = $EnvPath
|
|
if ($Mode -eq 'restore') { $source = $Path }
|
|
$content = Get-Content -LiteralPath $source -Raw -Encoding UTF8
|
|
$active = Get-EnvValue $content 'CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID'
|
|
$ring = Get-EnvValue $content 'CONFIG_CREDENTIAL_ENCRYPTION_KEYRING'
|
|
$legacy = Get-EnvValue $content 'CONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT'
|
|
if (-not $active -or -not $ring) { throw 'Incomplete original keyring; no replacement key will be generated' }
|
|
if (-not $legacy) { $legacy = 'false' }
|
|
$keyContent = "CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID=$active`nCONFIG_CREDENTIAL_ENCRYPTION_KEYRING='$ring'`nCONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT=$legacy`n"
|
|
foreach ($name in @('CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY','CONFIG_JWT_SECRET')) {
|
|
$match = [regex]::Match($content, "(?m)^$name=(.*)$")
|
|
if ($match.Success) { $keyContent += $match.Value.TrimEnd("`r") + "`n" }
|
|
elseif ($Mode -eq 'export') { $keyContent += "$name=`n" }
|
|
}
|
|
$tmp = $EnvPath + '.credential-keys.' + [Guid]::NewGuid().ToString('N')
|
|
try {
|
|
[System.IO.File]::WriteAllText($tmp, '', (New-Object System.Text.UTF8Encoding($false)))
|
|
Protect-CredentialFile $tmp
|
|
[System.IO.File]::WriteAllText($tmp, $keyContent, (New-Object System.Text.UTF8Encoding($false)))
|
|
Initialize-SecurityEnv -Path $tmp -Mode upgrade *> $null
|
|
if ($Mode -eq 'export') {
|
|
$reservation = [System.IO.File]::Open($Path, 'CreateNew', 'Write', 'None'); $reservation.Dispose()
|
|
Protect-CredentialFile $Path
|
|
[System.IO.File]::WriteAllText($Path, $keyContent, (New-Object System.Text.UTF8Encoding($false)))
|
|
Write-Host 'Exported full keyring, including old keys. Store privately and separately from database backups.'
|
|
} else {
|
|
$original = Get-Content -LiteralPath $EnvPath -Raw -Encoding UTF8
|
|
$previous = $EnvPath + '.pre-credential-restore.' + [Guid]::NewGuid().ToString('N')
|
|
[System.IO.File]::WriteAllText($previous, '', (New-Object System.Text.UTF8Encoding($false)))
|
|
Protect-CredentialFile $previous
|
|
[System.IO.File]::WriteAllText($previous, $original, (New-Object System.Text.UTF8Encoding($false)))
|
|
foreach ($name in @('CONFIG_CREDENTIAL_ENCRYPTION_ACTIVE_KEY_ID','CONFIG_CREDENTIAL_ENCRYPTION_KEYRING','CONFIG_CREDENTIAL_ALLOW_LEGACY_PLAINTEXT','CONFIG_SECURITY_CONFIG_ENCRYPTION_KEY','CONFIG_JWT_SECRET')) {
|
|
$saved = [regex]::Match($keyContent, "(?m)^$name=(.*)$")
|
|
if (-not $saved.Success) { continue }
|
|
$literalLine = $saved.Value.TrimEnd("`r") + "`r`n"
|
|
$pattern = "(?m)^$name=.*(?:\r?\n)?"
|
|
if ([regex]::IsMatch($original, $pattern)) {
|
|
$evaluator = [System.Text.RegularExpressions.MatchEvaluator]{ param($match) return $literalLine }.GetNewClosure()
|
|
$original = [regex]::Replace($original, $pattern, $evaluator)
|
|
} else { $original = $original.TrimEnd("`r","`n") + "`r`n" + $literalLine }
|
|
}
|
|
[System.IO.File]::WriteAllText($tmp, $original, (New-Object System.Text.UTF8Encoding($false)))
|
|
Move-Item -LiteralPath $tmp -Destination $EnvPath -Force
|
|
Protect-CredentialFile $EnvPath
|
|
Write-Host 'Restored original keyring; other configuration preserved. Recreate all related containers before database restore.'
|
|
}
|
|
} finally { if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force } }
|