feat(deploy): 补齐凭据加密接入迁移和密钥备份恢复
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
Test deployment public URL (Linux) / test-linux (pull_request) Canceled after 0s
Test start.ps1 (Windows) / test-windows (pull_request) Canceled after 0s
Test deployment public URL (Linux) / test-linux (push) Canceled after 0s
Test start.ps1 (Windows) / test-windows (push) Canceled after 0s
Test deployment public URL (Linux) / test-linux (pull_request) Canceled after 0s
Test start.ps1 (Windows) / test-windows (pull_request) Canceled after 0s
首次安装默认生成独立加密密钥,更新旧部署保留原配置,通过显式开关进入迁移窗口。增加生产迁移包装入口和完整密钥环备份恢复工具,并说明旧镜像升级、严格迁移、轮换及还原流程。 已通过 Bash 安全环境及密钥备份回归;对应 PowerShell 5.1 和 7 脚本已在 Windows 原生环境各通过 49 项验收。宁波服务器验收尚未执行。
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
#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 } }
|
||||
Reference in New Issue
Block a user