PowerShell 技能连载 - 凭据安全管理

适用于 PowerShell 7.0 及以上版本

在自动化脚本中硬编码密码是最常见也最危险的安全隐患之一。一段包含明文密码的脚本一旦被提交到版本控制系统,就会永久留在 Git 历史中,即使后续删除也无济于事。据统计,GitHub 上每天有数千个包含泄露凭据的提交被安全研究员发现。对于企业运维团队来说,一个泄露的服务账号密码可能意味着整个基础设施面临风险。

PowerShell 提供了完善的凭据安全管理机制。从传统的 PSCredential + SecureString 方案,到现代化的 SecretManagement 模块生态,再到凭据轮换与审计的完整生命周期管理,PowerShell 生态已经覆盖了凭据安全的各个环节。合理运用这些工具,可以确保密码永远不会以明文形式出现在脚本、日志或配置文件中。

本文将从三个维度展开:使用 PSCredential 和安全字符串进行本地凭据保护;通过 SecretManagement 模块实现跨平台密钥管理;以及构建凭据轮换与审计机制,形成完整的凭据安全闭环。

PSCredential 与安全字符串

PSCredential 是 PowerShell 中封装用户名和密码的标准对象,密码在内部以 SecureString 形式存储,不会以明文暴露在内存中。结合 Windows 的 DPAPI(数据保护 API),可以将凭据安全地序列化到文件,供后续脚本复用。

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
# 方式一:交互式创建凭据(适合手动执行场景)
$cred = Get-Credential -Message '请输入服务账号凭据'

# 方式二:通过代码创建凭据(适合自动化场景)
$plainPassword = 'My$ecureP@ssw0rd!2026'
$securePassword = $plainPassword | ConvertTo-SecureString -AsPlainText -Force
$cred = [System.Management.Automation.PSCredential]::new('svcautomation', $securePassword)

# 将凭据安全导出到文件(DPAPI 加密,仅当前用户可解密)
$cred | Export-Clixml -Path './svcautomation.cred.xml'

# 从文件导入凭据
$loadedCred = Import-Clixml -Path './svcautomation.cred.xml'

# 使用 SecureString 的 DPAPI 加密/解密
$encrypted = $plainPassword | ConvertTo-SecureString -AsPlainText -Force |
ConvertFrom-SecureString

# 将加密字符串保存到文件
$encrypted | Set-Content -Path './encrypted-password.txt'

# 在另一台机器或用户上下文中解密(需配合 Key 参数使用 AES 对称加密)
$key = (1..16 | ForEach-Object { Get-Random -Minimum 0 -Maximum 255 })
$encryptedWithKey = $plainPassword | ConvertTo-SecureString -AsPlainText -Force |
ConvertFrom-SecureString -Key $key

Write-Host "用户名: $($loadedCred.UserName)"
Write-Host "密码长度: $($loadedCred.GetNetworkCredential().Password.Length) 个字符"
Write-Host "加密字符串前 40 字符: $($encrypted.Substring(0, 40))..."

执行结果示例:

1
2
3
用户名: svcautomation
密码长度: 20 个字符
加密字符串前 40 字符: 01000000d08c9ddf0115d1118c7a00c04f...

需要注意的是,不带 -Key 参数的 ConvertFrom-SecureString 使用 Windows DPAPI 加密,加密后的数据只能在同一台机器的同一用户账户下解密。如果需要跨机器传输,必须使用 -Key 参数指定 AES 密钥。

SecretManagement 模块

Microsoft.PowerShell.SecretManagement 是微软官方推出的密钥管理框架,采用”核心模块 + 扩展保管库”的架构。核心模块提供统一的读写接口,而实际的密钥存储由扩展库实现,支持 Windows Credential Manager、Azure Key Vault、KeePass、HashiCorp Vault 等多种后端。脚本代码面向统一 API 编写,不必关心底层存储细节。

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
# 安装核心模块和扩展保管库
Install-Module -Name Microsoft.PowerShell.SecretManagement -Force -Scope CurrentUser
Install-Module -Name Microsoft.PowerShell.SecretStore -Force -Scope CurrentUser

# 注册本地 SecretStore 保管库
Register-SecretVault -Name 'LocalDevVault' -ModuleName Microsoft.PowerShell.SecretStore

# 配置保管库策略(需要密码才能访问)
Set-SecretStoreConfiguration -Authentication Password -Interaction None

# 存储各种类型的密钥
Set-Secret -Name 'DatabaseConnection' -Secret 'Server=db01;User=app;Password=S3cure!;' -Vault LocalDevVault
Set-Secret -Name 'ApiKey' -Secret 'sk-proj-abc123def456ghi789' -Vault LocalDevVault
Set-Secret -Name 'ServiceAccount' -Secret (
[System.Management.Automation.PSCredential]::new(
'DOMAIN\svcautomation',
('P@ssw0rd!' | ConvertTo-SecureString -AsPlainText -Force)
)
) -Vault LocalDevVault

# 读取密钥
$dbConn = Get-Secret -Name 'DatabaseConnection' -Vault LocalDevVault
$apiKey = Get-Secret -Name 'ApiKey' -Vault LocalDevVault
$svcCred = Get-Secret -Name 'ServiceAccount' -Vault LocalDevVault

# 列出所有密钥名称(不暴露值)
Get-SecretInfo -Vault LocalDevVault | Format-Table Name, Type

# 删除不需要的密钥
Remove-Secret -Name 'ApiKey' -Vault LocalDevVault

# 查看已注册的保管库信息
Get-SecretVault | Format-Table Name, ModuleName, IsDefault

执行结果示例:

1
2
3
4
5
6
7
8
9
Name                Type
---- ----
DatabaseConnection SecureString
ApiKey SecureString
ServiceAccount PSCredential

Name ModuleName IsDefault
---- ---------- ---------
LocalDevVault Microsoft.PowerShell.SecretStore False

SecretManagement 的优势在于扩展性。如果未来需要从本地文件迁移到 Azure Key Vault 或 HashiCorp Vault,只需注册新的保管库并迁移数据,业务脚本中的 Get-Secret 调用完全不需要修改。

凭据轮换与审计

凭据安全不止于安全存储,还需要定期轮换密码、记录使用日志并设置过期告警。以下代码构建了一个简易的凭据生命周期管理系统,可以自动生成强密码、更新保管库中的密钥,并记录完整的审计日志。

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 凭据轮换与审计管理系统

# 生成强随机密码
function New-RandomPassword {
[CmdletBinding()]
[OutputType([string])]
param(
[int]$Length = 24,
[int]$NonAlphaNumeric = 6
)
$assembly = [System.Reflection.Assembly]::LoadWithPartialName('System.Web')
$password = [System.Web.Security.Membership]::GeneratePassword($Length, $NonAlphaNumeric)
return $password
}

# 凭据审计日志(追加式 JSON 日志)
function Write-CredentialAuditLog {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$SecretName,
[Parameter(Mandatory)]
[ValidateSet('Create', 'Read', 'Rotate', 'Delete')]
[string]$Action,
[string]$VaultName = 'LocalDevVault',
[string]$Operator = $env:USERNAME
)
$logEntry = [ordered]@{
Timestamp = Get-Date -Format 'yyyy-MM-ddTHH:mm:ss.fffZ'
Action = $Action
SecretName = $SecretName
VaultName = $VaultName
Operator = $Operator
Machine = $env:COMPUTERNAME
}
$logPath = './credential-audit.json'
$existing = @()
if (Test-Path $logPath) {
$existing = Get-Content $logPath -Raw | ConvertFrom-Json
if ($existing -isnot [array]) { $existing = @($existing) }
}
$existing += $logEntry
$existing | ConvertTo-Json -Depth 5 | Set-Content $logPath -Encoding UTF8
}

# 凭据轮换:生成新密码并更新保管库
function Invoke-CredentialRotation {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$SecretName,
[string]$VaultName = 'LocalDevVault'
)
$newPassword = New-RandomPassword -Length 32 -NonAlphaNumeric 8
$secureNew = $newPassword | ConvertTo-SecureString -AsPlainText -Force
Set-Secret -Name $SecretName -Secret $secureNew -Vault $VaultName
Write-CredentialAuditLog -SecretName $SecretName -Action Rotate -VaultName $VaultName
Write-Host "[$SecretName] 密码已轮换,新密码长度: $($newPassword.Length) 字符"
}

# 检查凭据是否即将过期(基于审计日志中的最后轮换时间)
function Test-CredentialExpiry {
[CmdletBinding()]
param(
[int]$ExpireDays = 90
)
$logPath = './credential-audit.json'
if (-not (Test-Path $logPath)) {
Write-Warning '审计日志文件不存在,无法检查凭据过期状态'
return
}
$logs = Get-Content $logPath -Raw | ConvertFrom-Json
$rotateLogs = $logs | Where-Object { $_.Action -eq 'Rotate' }
$latestRotations = $rotateLogs | Group-Object SecretName | ForEach-Object {
$latest = ($_.Group | Sort-Object Timestamp -Descending | Select-Object -First 1)
[PSCustomObject]@{
SecretName = $_.Name
LastRotated = [datetime]$latest.Timestamp
DaysSince = [math]::Floor((Get-Date) - [datetime]$latest.Timestamp)
ShouldRotate = ((Get-Date) - [datetime]$latest.Timestamp).TotalDays -gt $ExpireDays
}
}
$latestRotations | Format-Table SecretName, LastRotated, DaysSince, ShouldRotate
$expired = $latestRotations | Where-Object { $_.ShouldRotate }
if ($expired) {
Write-Warning "以下凭据已超过 $ExpireDays 天未轮换:$($expired.SecretName -join ', ')"
}
}

# 执行轮换并记录审计日志
Invoke-CredentialRotation -SecretName 'DatabaseConnection' -VaultName LocalDevVault
Invoke-CredentialRotation -SecretName 'ServiceAccount' -VaultName LocalDevVault

# 检查凭据过期状态
Test-CredentialExpiry -ExpireDays 90

执行结果示例:

1
2
3
4
5
6
7
[DatabaseConnection] 密码已轮换,新密码长度: 32 字符
[ServiceAccount] 密码已轮换,新密码长度: 32 字符

SecretName LastRotated DaysSince ShouldRotate
---------- ------------ --------- ------------
DatabaseConnection 2026-04-16T10:30:00.000Z 0 False
ServiceAccount 2026-04-16T10:30:01.000Z 0 False

注意事项

  • 绝不硬编码密码:脚本中出现的任何明文密码都是安全隐患,应始终通过 PSCredentialSecureStringSecretManagement 模块来管理凭据。
  • DPAPI 加密的局限性:不带 -Key 参数的 ConvertFrom-SecureString 使用 Windows DPAPI,加密数据绑定于当前机器和用户账户,无法跨机器解密。跨机器场景请使用 -Key 参数配合 AES 对称加密。
  • SecretStore 密码保护:生产环境中务必将 SecretStore 配置为需要密码访问(Set-SecretStoreConfiguration -Authentication Password),不要使用 None 模式,否则任何能登录的用户都能读取密钥。
  • 审计日志的存储安全:凭据审计日志本身也包含敏感操作记录,应存放在受保护的位置,并设置适当的 NTFS 权限或文件加密,防止未授权访问。
  • 定期轮换密码:建议建立密码轮换策略,至少每 90 天轮换一次服务账号密码。轮换后确保所有依赖该凭据的自动化任务都使用更新后的密钥。
  • 密钥传输安全:在 CI/CD 管线中使用密钥时,应通过安全的管线变量或环境变量传递,避免将密钥写入磁盘文件或打印到控制台输出中。

PowerShell 技能连载 - Azure Key Vault 密钥管理

适用于 PowerShell 5.1 及以上版本

背景引入

在现代云原生应用的运维与开发中,密钥管理是安全体系中不可或缺的一环。数据库连接字符串、API 密钥、证书私钥等敏感信息如果以明文形式存储在代码仓库或配置文件中,一旦泄露就会造成严重的安全事故。Azure Key Vault 正是微软 Azure 平台提供的集中式密钥管理服务,它可以安全地存储和管理密钥(Key)、机密(Secret)及证书(Certificate)。

对于 PowerShell 用户而言,通过 Az.KeyVault 模块可以方便地完成 Key Vault 的创建、密钥的读写、访问策略的配置等操作。无论是日常运维脚本中拉取数据库密码,还是在 CI/CD 流水线中注入签名证书,PowerShell 都提供了简洁高效的 cmdlet 来实现这些需求。

本文将围绕实际场景,演示如何使用 PowerShell 完成 Azure Key Vault 的创建、Secret 的增删改查、批量操作以及访问权限控制。

前置准备

在开始操作 Key Vault 之前,需要确保已安装 Azure PowerShell 模块并完成登录认证。

1
2
3
4
5
6
7
8
# 安装 Az 模块(如果尚未安装)
Install-Module -Name Az -Repository PSGallery -Scope CurrentUser -Force

# 登录 Azure 账户(会弹出浏览器窗口进行交互式认证)
Connect-AzAccount

# 查看当前订阅信息,确认上下文正确
Get-AzContext | Select-Object Account, Subscription, Tenant

登录成功后,终端会显示当前账户的订阅和租户信息,确认是你期望操作的 Azure 订阅即可继续。

1
2
3
Account        SubscriptionName   TenantId
------- ---------------- --------
user@demo.com MySubscription xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

创建 Key Vault

Azure Key Vault 需要一个全局唯一的名称,并且必须指定资源组和所在区域。下面通过 PowerShell 一键完成创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义变量
$resourceGroupName = "rg-keyvault-demo"
$location = "EastAsia"
$vaultName = "kv-demo-$(Get-Random -Minimum 1000 -Maximum 9999)"

# 创建资源组(如果不存在)
New-AzResourceGroup -Name $resourceGroupName -Location $location -Force

# 创建 Key Vault
# 启用软删除(Soft Delete)和清除保护(Purge Protection)以增强安全性
$newVaultSplat = @{
VaultName = $vaultName
ResourceGroupName = $resourceGroupName
Location = $location
EnableSoftDelete = $true
EnablePurgeProtection = $true
Sku = "Standard"
}
$vault = New-AzKeyVault @newVaultSplat

Write-Host "Key Vault 创建成功: $($vault.VaultName)"
Write-Host "Vault URI: $($vault.VaultUri)"

这段脚本首先创建了一个资源组作为容器,然后在其中创建 Key Vault。EnableSoftDelete 参数确保删除的密钥可以在保留期内恢复,EnablePurgeProtection 则防止密钥被立即永久清除。

1
2
Key Vault 创建成功: kv-demo-7421
Vault URI: https://kv-demo-7421.vault.azure.net/

管理机密(Secret)

Key Vault 最常见的用途就是存储和管理机密信息。下面演示如何创建、读取、更新和删除 Secret。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 创建一个数据库连接字符串作为 Secret
$secretValue = ConvertTo-SecureString -String "Server=myserver.database.windows.net;Database=mydb;User=admin;Password=P@ssw0rd!" -AsPlainText -Force

$setSecretSplat = @{
VaultName = $vaultName
Name = "DatabaseConnectionString"
SecretValue = $secretValue
ContentType = "text/plain"
Tag = @{ Environment = "Production"; Owner = "DBA-Team" }
}
$secret = Set-AzKeyVaultSecret @setSecretSplat

Write-Host "Secret 创建成功"
Write-Host "名称: $($secret.Name)"
Write-Host "版本: $($secret.Version)"
Write-Host "过期时间: $($secret.Expires)"

# 读取 Secret 的值
$retrievedSecret = Get-AzKeyVaultSecret -VaultName $vaultName -Name "DatabaseConnectionString"
$plainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($retrievedSecret.SecretValue)
)
Write-Host "读取到的连接字符串: $plainText"

注意,Key Vault 返回的 Secret 值始终是 SecureString 类型。如果你需要在脚本中获取明文值(例如传递给数据库驱动),需要通过 Marshal 类进行转换。在实际生产环境中,应尽量避免将明文输出到控制台。

1
2
3
4
5
6
Secret 创建成功
名称: DatabaseConnectionString
版本: 1a2b3c4d5e6f7890abcdef1234567890
过期时间:

读取到的连接字符串: Server=myserver.database.windows.net;Database=mydb;User=admin;Password=P@ssw0rd!

批量管理 Secret

在真实项目中,我们往往需要一次性管理多个配置项。例如将应用的所有环境变量从本地配置文件批量导入到 Key Vault 中。下面演示一个批量写入和批量读取的完整流程。

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
# 定义多个配置项(模拟从配置文件读取)
$configItems = @(
@{ Name = "App-Api-Key"; Value = "sk-api-20251023-abcdefg123456" }
@{ Name = "App-Jwt-Secret"; Value = "my-super-secret-jwt-key-2025" }
@{ Name = "App-Redis-Connection"; Value = "redis://cache.redis.cache.windows.net:6380,password=xxx" }
@{ Name = "App-Storage-Key"; Value = "storageAccountAccessKeyXYZ789" }
@{ Name = "App-SendGrid-Key"; Value = "SG.sendgridapikey123456" }
)

# 批量写入 Key Vault
foreach ($item in $configItems) {
$secureValue = ConvertTo-SecureString -String $item.Value -AsPlainText -Force

$setSplat = @{
VaultName = $vaultName
Name = $item.Name
SecretValue = $secureValue
}
Set-AzKeyVaultSecret @setSplat | Out-Null
Write-Host "已写入 Secret: $($item.Name)"
}

Write-Host "`n批量写入完成,共 $($configItems.Count) 个 Secret"

# 批量读取并汇总
Write-Host "`n--- 当前 Key Vault 中的所有 Secret ---"
$allSecrets = Get-AzKeyVaultSecret -VaultName $vaultName

foreach ($s in $allSecrets) {
$detail = Get-AzKeyVaultSecret -VaultName $vaultName -Name $s.Name
$tagsStr = ""
if ($detail.Tags) {
$tagParts = @()
foreach ($key in $detail.Tags.Keys) {
$tagParts += "$key=$($detail.Tags[$key])"
}
$tagsStr = $tagParts -join ", "
}
Write-Host ("{0,-30} 创建于: {1}" -f $s.Name, $detail.Created.ToString("yyyy-MM-dd HH:mm"))
}

这段代码首先定义了一个配置数组,然后通过 foreach 循环逐个写入 Key Vault。读取时先获取所有 Secret 的列表,再逐个获取详细信息。这种方式适合在应用部署流水线中做配置初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
已写入 Secret: App-Api-Key
已写入 Secret: App-Jwt-Secret
已写入 Secret: App-Redis-Connection
已写入 Secret: App-Storage-Key
已写入 Secret: App-SendGrid-Key

批量写入完成,共 5 个 Secret

--- 当前 Key Vault 中的所有 Secret ---
DatabaseConnectionString 创建于: 2025-10-23 08:15
App-Api-Key 创建于: 2025-10-23 08:16
App-Jwt-Secret 创建于: 2025-10-23 08:16
App-Redis-Connection 创建于: 2025-10-23 08:16
App-Storage-Key 创建于: 2025-10-23 08:16
App-SendGrid-Key 创建于: 2025-10-23 08:16

配置访问策略

Key Vault 默认采用访问策略(Access Policy)模型来控制谁可以执行什么操作。创建者自动拥有全部权限,但对于团队协作场景,需要精确地为不同角色分配最小权限。

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
# 获取当前用户或服务主体的对象 ID
$currentUser = Get-AzADUser -SignedIn

# 为当前用户设置只读权限(Get 和 List)
$readOnlySplat = @{
VaultName = $vaultName
UserPrincipalName = $currentUser.UserPrincipalName
PermissionsToSecrets = "Get", "List"
}
Set-AzKeyVaultAccessPolicy @readOnlySplat

Write-Host "已为用户 $($currentUser.UserPrincipalName) 设置只读权限"

# 为另一个团队成员设置完整 Secret 权限
$devOpsUpn = "devops@demo.com"
$devOpsSplat = @{
VaultName = $vaultName
UserPrincipalName = $devOpsUpn
PermissionsToSecrets = "Get", "List", "Set", "Delete", "Recover", "Backup", "Restore", "Purge"
}
Set-AzKeyVaultAccessPolicy @devOpsSplat

Write-Host "已为用户 $devOpsUpn 设置完整 Secret 管理权限"

# 查看当前所有访问策略
$vault = Get-AzKeyVault -VaultName $vaultName
Write-Host "`n--- 当前访问策略 ---"
foreach ($policy in $vault.AccessPolicies) {
$secrets = $policy.PermissionsToSecrets -join ", "
Write-Host ("对象ID: {0} | Secret权限: {1}" -f $policy.ObjectId, $secrets)
}

在实际项目中,建议遵循最小权限原则:运维人员只授予 GetList 权限,密钥管理员才授予 SetDelete 等写入权限。对于应用程序,可以使用托管标识(Managed Identity)代替服务主体,避免在代码中硬编码凭据。

1
2
3
4
5
6
7
已为用户 user@demo.com 设置只读权限
已为用户 devops@demo.com 设置完整 Secret 管理权限

--- 当前访问策略 ---
对象ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | Secret权限: Get, List, Set, Delete, Recover, Backup, Restore, Purge
对象ID: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy | Secret权限: Get, List
对象ID: zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz | Secret权限: Get, List, Set, Delete, Recover, Backup, Restore, Purge

设置 Secret 过期与版本管理

Key Vault 支持 Secret 的自动过期和版本管理。当密钥需要定期轮换时(例如每 90 天更换一次 API 密钥),可以通过设置过期时间来实现自动提醒。

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
# 为已有的 Secret 设置过期时间
$expireDate = (Get-Date).AddDays(90)

$secretValue = ConvertTo-SecureString -String "sk-new-api-key-$(Get-Random -Minimum 100000 -Maximum 999999)" -AsPlainText -Force

$setExpiringSplat = @{
VaultName = $vaultName
Name = "App-Api-Key"
SecretValue = $secretValue
Expires = $expireDate
ContentType = "text/plain"
Tag = @{ RotatedBy = "PowerShell-Script"; RotationCycle = "90-days" }
}
$newVersion = Set-AzKeyVaultSecret @setExpiringSplat

Write-Host "Secret 已更新,新版本过期时间: $($newVersion.Expires.ToString('yyyy-MM-dd HH:mm'))"

# 查看某个 Secret 的所有历史版本
$allVersions = Get-AzKeyVaultSecret -VaultName $vaultName -Name "App-Api-Key" -IncludeVersions

Write-Host "`n--- App-Api-Key 版本历史 ---"
foreach ($ver in $allVersions) {
$status = if ($ver.Expires -and $ver.Expires -lt (Get-Date)) { "已过期" } else { "有效" }
Write-Host ("版本: {0} | 状态: {1} | 创建: {2}" -f $ver.Version.Substring(0, 8), $status, $ver.Created.ToString("yyyy-MM-dd HH:mm"))
}

每次对同一个 Secret 名称调用 Set-AzKeyVaultSecret,都会生成一个新版本。旧版本不会被自动删除,你可以随时回滚到之前的版本。这在密钥轮换出现问题时非常有用。

1
2
3
4
5
Secret 已更新,新版本过期时间: 2026-01-21 08:30

--- App-Api-Key 版本历史 ---
版本: 1a2b3c4d | 状态: 有效 | 创建: 2025-10-23 08:16
版本: 5e6f7g8h | 状态: 有效 | 创建: 2025-10-23 08:30

注意事项

  1. 启用软删除和清除保护:创建 Key Vault 时务必启用 SoftDeletePurgeProtection。软删除允许在保留期内(默认 90 天)恢复误删的 Secret,清除保护则防止恶意永久删除。这两项是生产环境的最低安全要求。

  2. 避免在日志中泄露明文:Key Vault 返回的 Secret 值是 SecureString 类型,在脚本中读取后切勿通过 Write-Host 或日志输出明文。如果确实需要调试,可以在开发环境使用 Marshal 转换,但在生产脚本中应删除此类代码。

  3. 使用托管标识代替硬编码凭据:在 Azure VM、App Service 或 AKS 上运行的应用,应使用系统分配或用户分配的托管标识(Managed Identity)访问 Key Vault,避免在代码或环境变量中存储服务主体密码。

  4. 遵循最小权限原则:为每个用户和服务主体仅授予其所需的最小权限集合。只读应用只需 GetList,只有密钥管理员才需要 SetDelete 权限。定期审计访问策略,移除不再需要的权限。

  5. 定期轮换密钥:为所有 Secret 设置合理的过期时间(如 30 天到 90 天),并建立自动轮换流程。Key Vault 支持通过 Event Grid 事件触发 Azure Function 来实现密钥的自动轮换,减少人工介入。

  6. 网络隔离与防火墙:对于高安全要求的场景,可以配置 Key Vault 的网络防火墙规则,仅允许特定虚拟网络的流量访问。结合 Private Endpoint 可以确保密钥数据不会经过公共互联网。

PowerShell 技能连载 - 凭据管理与安全存储

适用于 PowerShell 5.1 及以上版本,SecretManagement 模块需要 PowerShell 7

脚本中的硬编码密码是安全隐患的头号来源。无论是数据库连接字符串中的密码、API 密钥还是 SSH 私钥,都应该使用安全的存储机制。PowerShell 提供了多层凭据管理方案——从基本的 PSCredential 对象到 SecureString,再到现代化的 SecretManagement 模块,可以满足从单机脚本到企业级自动化的所有需求。

本文将讲解凭据的安全创建、存储、使用,以及 Microsoft.PowerShell.SecretManagement 模块的使用。

PSCredential 基础

PSCredential 是 PowerShell 中表示用户名+密码的标准方式:

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
# 交互式创建凭据(弹出对话框)
$cred = Get-Credential -Message "输入数据库管理员凭据"
Write-Host "用户名:$($cred.UserName)"

# 从明文创建 SecureString(仅用于脚本内传递,不要存储)
$plainPassword = "MyPassword123!"
$securePassword = ConvertTo-SecureString $plainPassword -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("admin", $securePassword)

# 从凭据中提取密码(仅在需要明文密码时使用)
$password = $cred.GetNetworkCredential().Password
Write-Host "密码长度:$($password.Length) 字符"

# 在命令中使用凭据
# Invoke-Command -ComputerName SRV01 -Credential $cred -ScriptBlock { ... }

# 验证凭据是否有效(简单测试)
function Test-Credential {
param([PSCredential]$Credential)

try {
$context = New-Object System.DirectoryServices.DirectoryEntry(
"", $Credential.UserName, $Credential.GetNetworkCredential().Password
)
if ($context.Name) {
Write-Host "凭据有效:$($Credential.UserName)" -ForegroundColor Green
return $true
}
} catch {
Write-Host "凭据无效:$($_.Exception.Message)" -ForegroundColor Red
return $false
}
}

执行结果示例:

1
2
3
用户名:admin
密码长度:13 字符
凭据有效:CONTOSO\admin

安全文件存储

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
function Save-CredentialToFile {
<#
.SYNOPSIS
将凭据安全保存到文件(DPAPI 加密)
#>
param(
[Parameter(Mandatory)]
[string]$Name,

[Parameter(Mandatory)]
[PSCredential]$Credential,

[string]$Path = "$env:USERPROFILE\.creds"
)

if (-not (Test-Path $Path)) {
New-Item -Path $Path -ItemType Directory | Out-Null
}

$filePath = Join-Path $Path "$Name.xml"
$Credential | Export-Clixml -Path $filePath
Write-Host "凭据已保存到:$filePath" -ForegroundColor Green
}

function Get-CredentialFromFile {
<#
.SYNOPSIS
从文件加载凭据
#>
param(
[Parameter(Mandatory)]
[string]$Name,

[string]$Path = "$env:USERPROFILE\.creds"
)

$filePath = Join-Path $Path "$Name.xml"
if (-not (Test-Path $filePath)) {
Write-Error "凭据文件不存在:$filePath"
return $null
}

Import-Clixml -Path $filePath
}

# 使用示例
$dbCred = Get-Credential -Message "输入数据库凭据"
Save-CredentialToFile -Name "database" -Credential $dbCred

# 后续脚本中加载
$loadedCred = Get-CredentialFromFile -Name "database"
Write-Host "已加载凭据:$($loadedCred.UserName)"

执行结果示例:

1
2
凭据已保存到:C:\Users\admin\.creds\database.xml
已加载凭据:sa

注意Export-Clixml 使用 Windows DPAPI 加密,只有当前用户在同一台机器上才能解密。不要将 .creds 目录加入版本控制。

SecretManagement 模块

PowerShell 7 的 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 Microsoft.PowerShell.SecretManagement -Scope CurrentUser -Force
Install-Module -Name Microsoft.PowerShell.SecretStore -Scope CurrentUser -Force

# 注册本地 Secret Store 保管库
Register-SecretVault -Name "LocalVault" -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault

# 设置保管库密码(首次使用)
Set-SecretStorePassword

# 存储凭据
$dbCred = Get-Credential -Message "数据库凭据"
Set-Secret -Name "DBAdmin" -Secret $dbCred -Vault "LocalVault"

# 存储简单字符串(API 密钥)
Set-Secret -Name "GitHubToken" -Secret "ghp_xxxxxxxxxxxx" -Vault "LocalVault"
Set-Secret -Name "OpenAIKey" -Secret "sk-xxxxxxxxxxxx" -Vault "LocalVault"

# 查看已存储的密钥列表
Get-SecretInfo -Vault "LocalVault" |
Select-Object Name, Type |
Format-Table -AutoSize

# 获取密钥
$token = Get-Secret -Name "GitHubToken" -AsPlainText
Write-Host "Token 长度:$($token.Length)"

$cred = Get-Secret -Name "DBAdmin"
Write-Host "用户名:$($cred.UserName)"

# 在脚本中使用
function Connect-MyDatabase {
$dbCred = Get-Secret -Name "DBAdmin"
$connString = "Server=prod-db;User ID=$($dbCred.UserName);Password=$($dbCred.GetNetworkCredential().Password)"
# 使用 $connString 连接数据库
}

执行结果示例:

1
2
3
4
5
6
7
8
Name         Type
---- ----
DBAdmin PSCredential
GitHubToken String
OpenAIKey String

Token 长度:40
用户名:sa

Azure Key Vault 集成

对于企业环境,Azure Key Vault 是推荐的集中式密钥管理方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 安装 Azure Key Vault 扩展
Install-Module -Name Az.KeyVault -Scope CurrentUser -Force

# 注册 Azure Key Vault 作为 Secret 保管库
Register-SecretVault -Name "AzureKV" -ModuleName Az.KeyVault `
-VaultParameters @{ AZKVaultName = 'my-company-vault'; SubscriptionId = 'xxx-xxx' }

# 存储密钥到 Azure Key Vault
$apiCred = Get-Credential -Message "API 服务账户"
Set-Secret -Name "ApiServiceAccount" -Secret $apiCred -Vault "AzureKV"

# 从 Azure Key Vault 获取密钥
$apiCred = Get-Secret -Name "ApiServiceAccount" -Vault "AzureKV"

执行结果示例:

1
# 首次访问时需要 Azure 认证

环境变量传递敏感信息

在 CI/CD 和容器化环境中,环境变量是传递凭据的常用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 从环境变量创建凭据
$dbUser = $env:DB_USER
$dbPass = $env:DB_PASSWORD | ConvertTo-SecureString -AsPlainText -Force
$dbCred = New-Object PSCredential($dbUser, $dbPass)

Write-Host "数据库用户:$($dbCred.UserName)"

# 安全地读取 API 密钥
$apiKey = $env:OPENAI_API_KEY
if (-not $apiKey) {
Write-Error "未设置环境变量 OPENAI_API_KEY"
exit 1
}
Write-Host "API Key 已加载(长度:$($apiKey.Length))"

# 检查所有必要的环境变量
$requiredEnvVars = @('DB_USER', 'DB_PASSWORD', 'API_KEY', 'SMTP_SERVER')
$missing = $requiredEnvVars | Where-Object { -not (Get-ChildItem env:$_ -ErrorAction SilentlyContinue) }

if ($missing) {
Write-Error "缺少环境变量:$($missing -join ', ')"
exit 1
}
Write-Host "所有环境变量已配置" -ForegroundColor Green

执行结果示例:

1
2
3
数据库用户:sa
API Key 已加载(长度:48
所有环境变量已配置

注意事项

  1. 不要硬编码密码:脚本中永远不应出现明文密码。使用环境变量、密钥库或交互式输入
  2. DPAPI 限制Export-Clixml 的 DPAPI 加密绑定到当前用户和机器,不能跨机器使用
  3. SecretStore 密码:忘记 SecretStore 密码后无法恢复已存储的密钥,务必妥善保管
  4. 日志脱敏:脚本输出中不要打印密码和密钥。使用 **** 替代敏感信息
  5. 最小权限原则:为自动化服务账户仅授予必要的最小权限
  6. 定期轮换:定期轮换 API 密钥和服务账户密码,并在密钥库中同步更新