适用于 PowerShell 7.0 及以上版本
在日常运维和自动化脚本编写中,硬编码密码、API Key 和数据库连接字符串是最常见的安全隐患之一。一旦脚本被误提交到公开仓库或在日志中泄露,敏感信息就会直接暴露。传统的做法是用 Read-Host -AsSecureString 手动输入,但这在自动化场景中并不适用。
PowerShell SecretManagement 模块的出现改变了这一局面。它提供了一套统一的密钥管理接口,通过扩展库机制支持多种后端存储:Windows Credential Manager、本地加密文件、Azure Key Vault、KeePass、HashiCorp Vault 等。脚本代码只需面向标准 API 编写,不必关心底层密钥存储在哪里。
本文将从基础安装配置讲起,逐步展示如何构建多保管库策略,以及在 CI/CD 和自动化场景中安全使用密钥的最佳实践。
SecretManagement 基础:注册保管库与存取密钥
SecretManagement 模块采用”核心模块 + 扩展库”的架构设计。核心模块 Microsoft.PowerShell.SecretManagement 提供统一的读写接口,而具体的存储后端由扩展库实现。我们首先安装核心模块和一个常用的本地扩展库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| Install-Module -Name Microsoft.PowerShell.SecretManagement -Force -Scope CurrentUser Install-Module -Name Microsoft.PowerShell.SecretStore -Force -Scope CurrentUser
Register-SecretVault -Name 'LocalDev' -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
Get-SecretVault | Format-Table Name, ModuleName, IsDefault
Set-Secret -Name 'ApiKey' -Secret 'sk-abc123def456ghi789' Set-Secret -Name 'DbPassword' -Secret (ConvertTo-SecureString 'P@ssw0rd!2026' -AsPlainText -Force) Set-Secret -Name 'ServiceAccount' -Secret ( [System.Management.Automation.PSCredential]::new( 'svc_automation', (ConvertTo-SecureString 'SvcP@ss2026!' -AsPlainText -Force) ) )
Get-SecretInfo -Vault 'LocalDev'
$apiKey = Get-Secret -Name 'ApiKey' -AsPlainText Write-Output "API Key 前缀: $($apiKey.Substring(0, 8))..."
$dbCred = Get-Secret -Name 'ServiceAccount' Write-Output "用户名: $($dbCred.UserName)"
|
1 2 3 4 5 6 7 8 9 10 11 12
| Name ModuleName IsDefault ---- ---------- --------- LocalDev Microsoft.PowerShell.SecretStore True
Name Type Vault ---- ---- ----- ApiKey String LocalDev DbPassword SecureString LocalDev ServiceAccount PSCredential LocalDev
API Key 前缀: sk-abc12... 用户名: svc_automation
|
可以看到 SecretManagement 支持三种密钥类型:普通字符串(String)、安全字符串(SecureString)和凭据对象(PSCredential)。注册保管库时使用 -DefaultVault 参数可以省略后续操作中反复指定保管库名称的麻烦。Get-SecretInfo 只返回元数据(名称和类型),不会解密实际内容,适合在脚本中做前置检查。
多保管库策略:本地开发与生产环境分离
在实际项目中,开发环境和生产环境通常使用不同的密钥存储方案。开发阶段可以用本地加密存储方便调试,而生产环境则需要对接企业级密钥管理服务。SecretManagement 的多保管库机制天然支持这种场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| Install-Module -Name Az.KeyVault -Force -Scope CurrentUser
Register-SecretVault -Name 'ProdKeyVault' -ModuleName Az.KeyVault -VaultParameters @{ AZKVaultName = 'prod-secrets-2026' SubscriptionId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }
Install-Module -Name SecretManagement.JustinGrote.CredMan -Force -Scope CurrentUser Register-SecretVault -Name 'CredMan' -ModuleName SecretManagement.JustinGrote.CredMan
Get-SecretVault | Format-Table Name, ModuleName, IsDefault
$envName = $env:PSENV ?? 'development'
$vaultName = switch ($envName) { 'production' { 'ProdKeyVault' } 'staging' { 'CredMan' } default { 'LocalDev' } }
Write-Output "当前环境: $envName, 使用保管库: $vaultName"
Set-Secret -Name "DbConnection-$envName" -Secret "Server=prod-db;Database=app;" -Vault $vaultName
$connection = Get-Secret -Name "DbConnection-$envName" -Vault $vaultName -AsPlainText Write-Output "连接字符串已获取,长度: $($connection.Length) 字符"
Set-SecretStorePassword -Interaction None -PasswordTimeout 3600
|
1 2 3 4 5 6 7 8
| Name ModuleName IsDefault ---- ---------- --------- LocalDev Microsoft.PowerShell.SecretStore True ProdKeyVault Az.KeyVault False CredMan SecretManagement.JustinGrote.CredMan False
当前环境: development, 使用保管库: LocalDev 连接字符串已获取,长度: 32 字符
|
多保管库策略的核心价值在于”代码不变,后端可换”。脚本中使用统一的 Get-Secret / Set-Secret 接口,只需要通过保管库名称区分环境。Set-SecretStorePassword 的 -PasswordTimeout 参数可以设置密码缓存时长,避免在批量操作中频繁弹出密码提示。
自动化脚本中的密钥安全使用
在 CI/CD 流水线和服务自动化场景中,密钥管理面临更多挑战:不能弹出交互式提示、需要支持密钥轮换、还要做好审计追踪。以下示例展示如何在自动化环境中安全地集成 SecretManagement。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
$secretStoreConfig = @{ Authentication = 'Password' PasswordTimeout = 0 Interaction = 'None' } Set-SecretStoreConfiguration @secretStoreConfig -Force
$vaultPassword = ConvertTo-SecureString $env:VAULT_PASSWORD -AsPlainText -Force Unlock-SecretStore -Password $vaultPassword
function Invoke-SecretRotation { param( [string]$SecretName, [string]$VaultName = 'LocalDev', [int]$Length = 32 )
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%' $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() $bytes = [byte[]]::new($Length) $rng.GetBytes($bytes) $newSecret = -join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] })
$oldValue = Get-Secret -Name $SecretName -Vault $VaultName -AsPlainText -ErrorAction SilentlyContinue if ($oldValue) { Set-Secret -Name "${SecretName}.backup.$(Get-Date -Format 'yyyyMMdd')" ` -Secret $oldValue -Vault $VaultName }
Set-Secret -Name $SecretName -Secret $newSecret -Vault $VaultName Write-Output "[$(Get-Date -Format 'HH:mm:ss')] 密钥 '$SecretName' 已轮换" }
$serviceSecrets = @{ 'DatabaseServer' = 'Svc_Database' 'MessageQueue' = 'Svc_MQ' 'StorageAccount' = 'Svc_Storage' }
$credentials = foreach ($entry in $serviceSecrets.GetEnumerator()) { $cred = Get-Secret -Name $entry.Value -Vault 'LocalDev' -ErrorAction Stop [PSCustomObject]@{ Service = $entry.Key UserName = if ($cred -is [pscredential]) { $cred.UserName } else { 'N/A' } HasSecret = $true } }
$credentials | Format-Table -AutoSize
$accessLog = @{ Timestamp = Get-Date -Format 'o' ScriptName = $MyInvocation.MyCommand.Name User = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name Secrets = $serviceSecrets.Values -join ', ' } $accessLog | ConvertTo-Json | Out-File -FilePath "./audit-$(Get-Date -Format 'yyyyMMdd').log" -Append Write-Output "审计日志已记录"
|
1 2 3 4 5 6 7 8 9 10 11
| [14:30:22] 密钥 'Svc_Database' 已轮换 [14:30:22] 密钥 'Svc_MQ' 已轮换 [14:30:22] 密钥 'Svc_Storage' 已轮换
Service UserName HasSecret ------- -------- --------- DatabaseServer svc_db True MessageQueue svc_mq True StorageAccount svc_storage True
审计日志已记录
|
这个模板展示了自动化场景的几个关键设计:通过 Unlock-SecretStore 用环境变量解锁保管库,避免交互式密码输入;Invoke-SecretRotation 函数在轮换密钥时自动备份旧值,方便紧急回滚;审计日志记录了谁在什么时候访问了哪些密钥,满足合规要求。
注意事项
必须保护保管库密码:SecretStore 的加密密码是整个安全链的起点。在 CI/CD 中应使用平台原生机制(如 GitHub Secrets、Azure Pipeline Variables)注入 VAULT_PASSWORD 环境变量,绝不能硬编码在脚本中。
区分密钥类型:Set-Secret 会根据传入值的类型自动推断。字符串会以 String 类型存储(Get-Secret -AsPlainText 直接可读),SecureString 和 PSCredential 则提供更高的安全等级。建议对高敏感度密钥使用 SecureString 或 PSCredential 类型。
扩展库的选择:SecretStore 扩展库适合个人开发和小团队,密钥以 AES-256 加密存储在本地文件中。企业环境应优先对接 Azure Key Vault 或 HashiCorp Vault,利用其访问控制、审计日志和自动轮换等高级功能。
密钥命名规范:建议使用 {项目}/{环境}/{用途} 的命名约定(如 MyApp/Prod/DbPassword),避免命名冲突,也方便批量查询和管理。
密钥轮换策略:定期轮换密钥是安全最佳实践。轮换时应先备份旧值、写入新值、验证新值可用后再删除备份。切勿在新值未验证通过时删除旧密钥。
模块版本兼容性:SecretManagement 的扩展库接口在 1.x 版本中保持稳定,但建议在 requirements.psd1 或 modules.json 中锁定版本号,避免自动化流水线中因模块自动更新引入兼容性问题。