PowerShell 技能连载 - 凭据管理

适用于 PowerShell 5.1 及以上版本

凭据管理的重要性

在日常运维和自动化脚本中,我们经常需要处理各种凭据:远程服务器的登录密码、API 密钥、数据库连接字符串等。如果直接将这些敏感信息以明文形式写在脚本中,不仅存在极大的安全隐患,也违反了绝大多数企业的安全合规要求。

PowerShell 提供了 PSCredential 对象来安全地封装用户名和密码。密码在 PSCredential 内部以 SecureString 形式存储,不会以明文暴露在内存中。结合 Windows 的 DPAPI(数据保护 API),我们可以将凭据安全地保存到文件,在后续脚本中重复使用,而无需每次手动输入。

本文将介绍从创建凭据、安全存储凭据到使用 Microsoft.PowerShell.SecretManagement 模块管理密钥的完整流程,帮助你建立一套规范的凭据管理方案。

创建 PSCredential 对象

最直接的方式是使用 Get-Credential cmdlet,它会弹出一个 Windows 对话框让用户输入用户名和密码。在无人值守的自动化场景中,我们可以通过代码手动构建 PSCredential 对象。以下示例展示了两种创建方式,并将密码转换为明文进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
# 方式一:交互式弹窗获取凭据
$cred = Get-Credential -Message "请输入远程服务器凭据"

# 方式二:在脚本中手动构建凭据(适合自动化场景)
$username = "AdminUser"
$password = ConvertTo-SecureString -String "MyP@ssw0rd!" -AsPlainText -Force
$cred = [System.Management.Automation.PSCredential]::new($username, $password)

# 查看凭据信息
Write-Host "用户名: $($cred.UserName)"
Write-Host "密码长度: $($cred.GetNetworkCredential().Password.Length) 个字符"
Write-Host "密码是否已加密: $($cred.Password.IsReadOnly())"
1
2
3
用户名: AdminUser
密码长度: 11 个字符
密码是否已加密: False

安全存储和读取凭据

直接在脚本中硬编码密码显然不安全。更好的做法是将密码以加密形式导出到文件,后续脚本从文件中读取。ConvertFrom-SecureString 利用当前用户的 DPAPI 证书对密码加密,导出的密文只有同一台机器上的同一个用户才能解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 将密码加密保存到文件
$secretFile = "$env:USERPROFILE\.secrets\remote-admin.xml"

# 确保目录存在
$dir = Split-Path -Path $secretFile -Parent
if (-not (Test-Path -Path $dir)) {
$null = New-Item -Path $dir -ItemType Directory -Force
}

# 创建凭据并导出加密密码
$username = "AdminUser"
$password = Read-Host -Prompt "请输入密码" -AsSecureString
$cred = [System.Management.Automation.PSCredential]::new($username, $password)

# 将加密后的密码保存到文件
$password | ConvertFrom-SecureString | Out-File -FilePath $secretFile -Encoding UTF8
Write-Host "凭据已安全保存到: $secretFile"

# 在另一个脚本中读取凭据
$encryptedPassword = Get-Content -Path $secretFile -Raw
$securePassword = $encryptedPassword | ConvertTo-SecureString
$restoredCred = [System.Management.Automation.PSCredential]::new($username, $securePassword)

Write-Host "凭据已恢复 - 用户名: $($restoredCred.UserName)"
1
2
凭据已安全保存到: C:\Users\admin\.secrets\remote-admin.xml
凭据已恢复 - 用户名: AdminUser

批量管理多个服务的凭据

在真实环境中,我们往往需要同时管理多组凭据:数据库账号、API 密钥、SSH 登录等。可以为每组凭据创建独立的加密文件,并编写一个统一的管理函数来简化操作。

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

[Parameter(Mandatory)]
[string]$UserName,

[Parameter(Mandatory)]
[securestring]$Password
)

$secretDir = Join-Path -Path $env:USERPROFILE -ChildPath ".secrets"
if (-not (Test-Path -Path $secretDir)) {
$null = New-Item -Path $secretDir -ItemType Directory -Force
}

$cred = [System.Management.Automation.PSCredential]::new($UserName, $Password)
$filePath = Join-Path -Path $secretDir -ChildPath "$Name.xml"

# 使用 Export-Clixml 保存整个凭据对象(包含用户名和加密密码)
$cred | Export-Clixml -Path $filePath -Encoding UTF8
Write-Host "[$Name] 凭据已保存到: $filePath"
}

function Get-Secret {
<#
.SYNOPSIS
从本地文件读取已加密的凭据
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Name
)

$filePath = Join-Path -Path $env:USERPROFILE -ChildPath ".secrets\$Name.xml"
if (-not (Test-Path -Path $filePath)) {
Write-Error "找不到名为 [$Name] 的凭据文件"
return
}

# Import-Clixml 自动利用 DPAPI 解密还原 PSCredential 对象
$cred = Import-Clixml -Path $filePath
Write-Host "[$Name] 凭据已加载 - 用户名: $($cred.UserName)"
return $cred
}

# 批量保存多个服务凭据
$services = @(
@{ Name = "sqlserver"; User = "sa" }
@{ Name = "ssh-gateway"; User = "deploy" }
@{ Name = "api-service"; User = "api-reader" }
)

foreach ($svc in $services) {
$securePwd = Read-Host -Prompt "请输入 $($svc.Name) 的密码" -AsSecureString
Save-Secret -Name $svc.Name -UserName $svc.User -Password $securePwd
}

# 验证:列出所有已保存的凭据
Write-Host "`n已保存的凭据列表:"
$secretFiles = Get-ChildItem -Path "$env:USERPROFILE\.secrets\*.xml"
foreach ($file in $secretFiles) {
$cred = Import-Clixml -Path $file.FullName
Write-Host " - $($file.BaseName): $($cred.UserName)"
}
1
2
3
4
5
6
7
8
[sqlserver] 凭据已保存到: C:\Users\admin\.secrets\sqlserver.xml
[ssh-gateway] 凭据已保存到: C:\Users\admin\.secrets\ssh-gateway.xml
[api-service] 凭据已保存到: C:\Users\admin\.secrets\api-service.xml

已保存的凭据列表:
- sqlserver: sa
- ssh-gateway: deploy
- api-service: api-reader

使用 SecretManagement 模块统一管理密钥

PowerShell 7 引入了 Microsoft.PowerShell.SecretManagementMicrosoft.PowerShell.SecretStore 模块,提供了一套跨平台的密钥管理框架。它支持将密钥存储在本地 SecretStore 中,也可以扩展对接 Azure Key Vault、KeePass 等外部密钥管理服务。

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

# 注册本地 SecretStore 作为密钥保管库
Register-SecretVault -Name "MyLocalVault" -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault

# 设置 SecretStore 密码(用于解锁保管库)
$storePassword = Read-Host -Prompt "设置 SecretStore 密码" -AsSecureString
Set-SecretStorePassword -NewPassword $storePassword -Password $storePassword

# 存储各种类型的密钥
Set-Secret -Name "DatabaseConnection" -Secret "Server=db01;Database=prod;User Id=app;Password=s3cret;"
Set-Secret -Name "ApiKey" -Secret "ak-12345-abcde-67890"
Set-Secret -Name "ServiceAccount" -Secret (Get-Credential -UserName "svc_automation" -Message "输入服务账号密码")

# 查询已存储的密钥信息
Write-Host "密钥保管库中存储的密钥列表:"
$secrets = Get-SecretInfo
foreach ($secret in $secrets) {
Write-Host " 名称: $($secret.Name) | 类型: $($secret.Type)"
}

# 在脚本中按需获取密钥
$dbConn = Get-Secret -Name "DatabaseConnection" -AsPlainText
Write-Host "`n数据库连接字符串长度: $($dbConn.Length) 个字符"

$apiCred = Get-Secret -Name "ServiceAccount"
Write-Host "服务账号用户名: $($apiCred.UserName)"
1
2
3
4
5
6
7
密钥保管库中存储的密钥列表:
名称: DatabaseConnection | 类型: String
名称: ApiKey | 类型: String
名称: ServiceAccount | 类型: PSCredential

数据库连接字符串长度: 68 个字符
服务账号用户名: svc_automation

注意事项

  1. DPAPI 作用域限制:使用 ConvertFrom-SecureStringExport-Clixml 加密的凭据只能在同一台机器上的同一个 Windows 用户账户下解密。如果需要跨机器使用,请使用 -Key 参数指定 AES 加密密钥,或者使用 SecretManagement 模块配合外部密钥管理服务。

  2. 避免密码明文传入ConvertTo-SecureString -AsPlainText -Force 虽然方便,但会暴露明文密码。在生产环境中,应优先使用 Get-CredentialRead-Host -AsSecureString 交互获取密码,或从已加密的文件中读取。

  3. SecureString 并非绝对安全SecureString 能防止密码在内存中以明文形式长期驻留,但不能防御具有管理员权限的恶意程序通过内存转储获取密码。在 Linux 和 macOS 上,SecureString 的加密保护能力弱于 Windows,建议结合操作系统级别的密钥管理工具使用。

  4. 文件权限控制:保存凭据文件的目录应设置严格的 NTFS 权限,仅允许当前用户访问。可以通过以下命令快速设置:icacls "$env:USERPROFILE\.secrets" /inheritance:r /grant:r "$env:USERNAME:(R,W)"

  5. SecretStore 密码管理Microsoft.PowerShell.SecretStore 默认要求在每次会话中输入密码解锁。如果自动化脚本需要无人值守运行,可以通过 Set-SecretStoreConfiguration -Authentication None 关闭密码提示,但这会降低安全性,请根据实际场景权衡。

  6. 定期轮换凭据:无论使用哪种存储方式,都应建立凭据轮换机制。建议在密钥管理流程中记录每个凭据的创建时间和过期时间,并在脚本中添加检查逻辑,对即将过期的凭据发出告警提醒。

PowerShell 技能连载 - 凭据自动轮换

适用于 PowerShell 5.1 及以上版本

在信息安全领域,凭据管理是最基础也最重要的环节之一。无论是服务账户密码、API 密钥还是数据库连接字符串,长期不变的秘密都是潜在的安全隐患。越来越多的安全合规标准(如 PCI DSS、SOC 2)要求定期轮换密码,人工操作不仅容易遗漏,还可能在交接过程中引入风险。

自动化凭据轮换可以确保密码按策略定期更换,并将新密码安全地同步到所有需要的地方。PowerShell 结合 Windows 凭据管理 API 和 Azure Key Vault 等服务,能够构建端到端的凭据轮换工作流。本文将介绍如何用 PowerShell 实现凭据的安全存储、自动生成和轮换管理。

安全生成随机密码

凭据轮换的第一步是生成足够强度的随机密码。.NET 的 System.Security.Cryptography.RNGCryptoServiceProvider 提供了密码学安全的随机数生成。

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
function New-RandomPassword {
<#
.SYNOPSIS
生成密码学安全的随机密码
.PARAMETER Length
密码长度(默认 20)
.PARAMETER ExcludeSpecial
是否排除特殊字符
#>
[CmdletBinding()]
param(
[int]$Length = 20,
[switch]$ExcludeSpecial
)

# 定义字符集
$lowercase = 'abcdefghijkmnopqrstuvwxyz'
$uppercase = 'ABCDEFGHJKLMNPQRSTUVWXYZ'
$digits = '23456789'
$special = '!@#$%^&*_-+='

if ($ExcludeSpecial) {
$charset = $lowercase + $uppercase + $digits
} else {
$charset = $lowercase + $uppercase + $digits + $special
}

# 使用密码学安全的随机数生成器
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$buffer = [byte[]]::new(1)

$password = [System.Text.StringBuilder]::new($Length)
for ($i = 0; $i -lt $Length; $i++) {
$rng.GetBytes($buffer)
$index = $buffer[0] % $charset.Length
[void]$password.Append($charset[$index])
}

# 确保密码包含各类字符
$pwd = $password.ToString()
$hasLower = $pwd -cmatch '[a-z]'
$hasUpper = $pwd -cmatch '[A-Z]'
$hasDigit = $pwd -cmatch '\d'

if (-not ($hasLower -and $hasUpper -and $hasDigit)) {
# 递归重新生成
return New-RandomPassword -Length $Length -ExcludeSpecial:$ExcludeSpecial
}

return $pwd
}

# 生成多个随机密码
1..3 | ForEach-Object {
New-RandomPassword -Length 24
}

执行结果示例:

1
2
3
k7Xt@mP9Rb3LwQ!hN2vD5jM8
Y4nF$8sHjK2pWq_Bv6TcAx9R
m3Lb7!NhDk9QwFp+Xr2GtY5J

使用 Windows 凭据管理器存储密码

Windows 凭据管理器(Credential Manager)是存储和管理凭据的系统级服务,支持持久化存储和访问控制。

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
96
function Set-StoredCredential {
<#
.SYNOPSIS
将凭据保存到 Windows 凭据管理器
.PARAMETER Target
凭据标识名称
.PARAMETER UserName
用户名
.PARAMETER Password
密码(明文,将由函数加密存储)
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Target,

[Parameter(Mandatory)]
[string]$UserName,

[Parameter(Mandatory)]
[securestring]$Password
)

# 将 SecureString 转换为 BSTR 再转换为明文(仅用于 API 调用)
$cred = New-Object System.Management.Automation.PSCredential($UserName, $Password)
$plainPassword = $cred.GetNetworkCredential().Password

# 使用 cmdkey 命令存储凭据
$arguments = "/generic:`"$Target`" /user:`"$UserName`" /pass:`"$plainPassword`""

$process = Start-Process -FilePath 'cmdkey.exe' `
-ArgumentList $arguments `
-NoNewWindow `
-Wait `
-PassThru

if ($process.ExitCode -eq 0) {
Write-Verbose "凭据已保存:$Target"
} else {
throw "保存凭据失败,退出码:$($process.ExitCode)"
}

# 清除内存中的明文密码
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR(
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
)
}

function Get-StoredCredential {
<#
.SYNOPSIS
从 Windows 凭据管理器读取凭据
.PARAMETER Target
凭据标识名称
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Target
)

# 使用 cmdkey 列出凭据
$output = cmdkey /list 2>&1
$found = $false

foreach ($line in $output) {
if ($line -match [regex]::Escape($Target)) {
$found = $true
break
}
}

if (-not $found) {
Write-Warning "未找到凭据:$Target"
return $null
}

# 使用 CredRead Win32 API 读取凭据
# 这里简化为通过 PSCredential 提示获取
try {
$cred = Get-StoredCredential -Target $Target -ErrorAction Stop
return $cred
} catch {
Write-Warning "读取凭据失败:$_"
return $null
}
}

# 保存凭据
$newPassword = New-RandomPassword -Length 24
$securePassword = ConvertTo-SecureString $newPassword -AsPlainText -Force

Set-StoredCredential -Target 'MyServiceAccount' `
-UserName 'svc_automation' `
-Password $securePassword `
-Verbose

执行结果示例:

1
详细: 凭据已保存:MyServiceAccount

Active Directory 服务账户密码轮换

企业环境中,服务账户的密码轮换需要同时更新 AD 中的密码和所有依赖该账户的应用程序配置。

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
function Reset-ADServiceAccountPassword {
<#
.SYNOPSIS
重置 AD 服务账户密码并记录变更
.PARAMETER SamAccountName
服务账户的 sAMAccountName
.PARAMETER NewPassword
新密码(SecureString)
.PARAMETER Server
域控制器名称
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$SamAccountName,

[Parameter(Mandatory)]
[securestring]$NewPassword,

[string]$Server
)

# 查找服务账户
$adUserParams = @{ Identity = $SamAccountName }
if ($Server) { $adUserParams['Server'] = $Server }

$user = Get-ADUser @adUserParams -Properties PasswordLastSet, Description

if (-not $user) {
throw "未找到账户:$SamAccountName"
}

Write-Verbose "找到账户:$($user.DistinguishedName)"
Write-Verbose "上次密码设置时间:$($user.PasswordLastSet)"

# 重置密码
$setParams = @{
Identity = $SamAccountName
NewPassword = $NewPassword
Reset = $true
}
if ($Server) { $setParams['Server'] = $Server }

Set-ADAccountPassword @setParams

# 确保账户未过期且未锁定
Unlock-ADAccount -Identity $SamAccountName -ErrorAction SilentlyContinue

[PSCustomObject]@{
AccountName = $SamAccountName
DistinguishedName = $user.DistinguishedName
PasswordLastSet = Get-Date
OldPasswordSet = $user.PasswordLastSet
Status = '密码已重置'
}
}

# 执行密码轮换
$newPwd = New-RandomPassword -Length 24
$securePwd = ConvertTo-SecureString $newPwd -AsPlainText -Force

$result = Reset-ADServiceAccountPassword `
-SamAccountName 'svc_automation' `
-NewPassword $securePwd `
-Server 'dc01.contoso.com' `
-Verbose

$result | Format-List

执行结果示例:

1
2
3
4
5
6
7
8
详细: 找到账户:CN=svc_automation,OU=Service Accounts,DC=contoso,DC=com
详细: 上次密码设置时间:2025/7/19 10:30:00

AccountName : svc_automation
DistinguishedName : CN=svc_automation,OU=Service Accounts,DC=contoso,DC=com
PasswordLastSet : 2025/8/19 08:00:15
OldPasswordSet : 2025/7/19 10:30:00
Status : 密码已重置

配置文件中的密码同步

密码轮换后,需要将新密码同步到所有使用该账户的应用程序配置文件中。

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
function Update-ConfigCredential {
<#
.SYNOPSIS
更新配置文件中的凭据信息
.PARAMETER ConfigPath
配置文件路径(JSON 格式)
.PARAMETER KeyPath
密码字段的 JSON 路径(如 'database.password')
.PARAMETER NewPassword
新密码
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ConfigPath,

[Parameter(Mandatory)]
[string]$KeyPath,

[Parameter(Mandatory)]
[string]$NewPassword
)

# 备份原始配置
$backupPath = "$ConfigPath.bak.$(Get-Date -Format 'yyyyMMdd-HHmmss')"
Copy-Item -Path $ConfigPath -Destination $backupPath -Force
Write-Verbose "已备份配置到:$backupPath"

# 读取并解析配置
$config = Get-Content $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json

# 沿路径导航到目标属性
$pathParts = $KeyPath -split '\.'
$obj = $config
for ($i = 0; $i -lt $pathParts.Count - 1; $i++) {
$obj = $obj.($pathParts[$i])
}

$oldValue = $obj.($pathParts[-1])
$obj.($pathParts[-1]) = $NewPassword

# 写回配置文件
$config | ConvertTo-Json -Depth 10 | Set-Content $ConfigPath -Encoding UTF8

[PSCustomObject]@{
ConfigFile = $ConfigPath
KeyPath = $KeyPath
BackupFile = $backupPath
OldLength = if ($oldValue) { $oldValue.Length } else { 0 }
NewLength = $NewPassword.Length
UpdatedAt = Get-Date
}
}

# 示例配置文件路径列表
$configFiles = @(
@{
Path = 'C:\App\config\appsettings.json'
Key = 'database.password'
}
@{
Path = 'C:\App\config\connection.json'
Key = 'serviceCredentials.password'
}
)

# 批量更新配置文件
foreach ($cfg in $configFiles) {
if (Test-Path $cfg.Path) {
$updateResult = Update-ConfigCredential `
-ConfigPath $cfg.Path `
-KeyPath $cfg.Key `
-NewPassword $newPwd `
-Verbose
$updateResult | Format-Table -AutoSize
} else {
Write-Warning "配置文件不存在:$($cfg.Path)"
}
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
详细: 已备份配置到:C:\App\config\appsettings.json.bak.20250819-080015

ConfigFile KeyPath BackupFile OldLength NewLength UpdatedAt
---------- ------- ----------- --------- --------- ---------
C:\App\config\appsettings.json database.password C:\App\config\appsettings.json.bak.20250819-080015 20 24 2025/8/19 8:00:15

详细: 已备份配置到:C:\App\config\connection.json.bak.20250819-080016

ConfigFile KeyPath BackupFile OldLength NewLength UpdatedAt
---------- ------- ----------- --------- --------- ---------
C:\App\config\connection.json serviceCredentials.password C:\App\config\connection.json.bak.20250819-080016 20 24 2025/8/19 8:00:16

完整的凭据轮换工作流

将上述各步骤整合为一个完整的轮换流程,并可定期通过任务计划程序自动执行。

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
96
97
98
99
function Invoke-CredentialRotation {
<#
.SYNOPSIS
执行完整的凭据轮换流程
.PARAMETER AccountName
服务账户名称
.PARAMETER ConfigFiles
需要更新密码的配置文件列表
.PARAMETER DcServer
域控制器
.PARAMETER CredentialTarget
Windows 凭据管理器中的标识
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$AccountName,

[hashtable[]]$ConfigFiles,

[string]$DcServer,

[string]$CredentialTarget
)

$rotationLog = [System.Collections.Generic.List[string]]::new()
$success = $true

try {
# 步骤 1:生成新密码
$rotationLog.Add("[INFO] 开始凭据轮换:$AccountName")
$newPassword = New-RandomPassword -Length 24
$securePassword = ConvertTo-SecureString $newPassword -AsPlainText -Force
$rotationLog.Add("[INFO] 已生成新密码(长度:$($newPassword.Length))")

# 步骤 2:重置 AD 密码
$adParams = @{
SamAccountName = $AccountName
NewPassword = $securePassword
}
if ($DcServer) { $adParams['Server'] = $DcServer }

$adResult = Reset-ADServiceAccountPassword @adParams -Verbose
$rotationLog.Add("[INFO] AD 密码已重置:$($adResult.DistinguishedName)")

# 步骤 3:更新配置文件
foreach ($cfg in $ConfigFiles) {
$updateResult = Update-ConfigCredential `
-ConfigPath $cfg.Path `
-KeyPath $cfg.Key `
-NewPassword $newPassword `
-Verbose
$rotationLog.Add("[INFO] 配置已更新:$($cfg.Path)")
}

# 步骤 4:更新凭据管理器
if ($CredentialTarget) {
Set-StoredCredential -Target $CredentialTarget `
-UserName $AccountName `
-Password $securePassword `
-Verbose
$rotationLog.Add("[INFO] 凭据管理器已更新:$CredentialTarget")
}

# 步骤 5:重启相关服务
$rotationLog.Add("[INFO] 凭据轮换完成")
} catch {
$success = $false
$rotationLog.Add("[ERROR] 轮换失败:$_")
throw
} finally {
# 写入轮换日志
$logDir = 'C:\Logs\CredentialRotation'
if (-not (Test-Path $logDir)) {
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
$logFile = Join-Path $logDir "rotation-$AccountName-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
$rotationLog | Set-Content $logFile -Encoding UTF8

[PSCustomObject]@{
Account = $AccountName
Success = $success
LogFile = $logFile
Timestamp = Get-Date
}
}
}

# 执行轮换
$result = Invoke-CredentialRotation `
-AccountName 'svc_automation' `
-DcServer 'dc01.contoso.com' `
-CredentialTarget 'MyServiceAccount' `
-ConfigFiles @(
@{ Path = 'C:\App\config\appsettings.json'; Key = 'database.password' }
) `
-Verbose

$result | Format-List

执行结果示例:

1
2
3
4
5
6
7
8
9
详细: 找到账户:CN=svc_automation,OU=Service Accounts,DC=contoso,DC=com
详细: 上次密码设置时间:2025/7/19 10:30:00
详细: 已备份配置到:C:\App\config\appsettings.json.bak.20250819-080020
详细: 凭据已保存:MyServiceAccount

Account : svc_automation
Success : True
LogFile : C:\Logs\CredentialRotation\rotation-svc_automation-20250819-080020.log
Timestamp : 2025/8/19 8:00:20

注意事项

  • 回滚机制:密码轮换失败时必须有回滚方案,建议在修改前备份旧密码(加密存储)和配置文件,确保可以快速恢复到轮换前的状态。
  • 服务重启:很多应用程序在启动时读取密码并缓存在内存中,密码轮换后需要重启相关服务才能生效,应在低峰期执行。
  • 加密传输:密码在网络上传输时应使用 LDAPS(端口 636)而非 LDAP(端口 389),避免明文密码被网络抓包截获。
  • 权限最小化:执行轮换的服务账户应仅有重置目标账户密码的权限,不应拥有 Domain Admin 等高权限,降低凭据泄露的影响范围。
  • 日志审计:所有轮换操作必须记录详细日志,包括操作时间、操作者、目标账户、操作结果,以备安全审计使用。
  • 密码复杂度:生成密码时应确保满足域密码策略要求(最小长度、复杂度、历史记录等),否则 Set-ADAccountPassword 会因策略违规而失败。

PowerShell 技能连载 - 安全编码实践

适用于 PowerShell 5.1 及以上版本

安全编码不是可选项,而是生产环境的基本要求。PowerShell 脚本经常处理凭据、连接字符串、API 密钥等敏感信息,如果处理不当,这些信息可能泄露到日志文件、版本控制系统、甚至被恶意代码利用。从凭据管理、输入验证、代码签名,到审计日志和合规检查,PowerShell 提供了完整的安全工具链。

本文将讲解 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 不要这样做——明文密码
# $password = "MyP@ssw0rd123"

# 安全方式 1:Get-Credential 交互式输入
$credential = Get-Credential -Message "输入服务账户凭据"
Write-Host "用户名:$($credential.UserName)"
Write-Host "密码长度:$($credential.GetNetworkCredential().Password.Length)"

# 安全方式 2:加密文件存储(DPAPI,仅限当前用户和机器)
function Save-SecureCredential {
param(
[Parameter(Mandatory)]
[string]$Name,

[Parameter(Mandatory)]
[System.Management.Automation.PSCredential]$Credential
)

$credDir = "$env:APPDATA\PowerShell\Credentials"
New-Item $credDir -ItemType Directory -Force | Out-Null

$Credential | Export-Clixml -Path "$credDir\$Name.xml" -Force
Write-Host "凭据已安全保存:$Name" -ForegroundColor Green
}

function Get-SecureCredential {
param([Parameter(Mandatory)][string]$Name)

$path = "$env:APPDATA\PowerShell\Credentials\$Name.xml"
if (-not (Test-Path $path)) {
throw "凭据文件不存在:$Name"
}
return Import-Clixml -Path $path
}

# 保存和读取凭据
$cred = Get-Credential -Message "输入数据库连接凭据"
Save-SecureCredential -Name "DBAdmin" -Credential $cred

$dbCred = Get-SecureCredential -Name "DBAdmin"
Write-Host "已加载凭据:$($dbCred.UserName)"

# 安全方式 3:Windows Credential Manager
Install-Module -Name CredentialManager -Force -Scope CurrentUser
New-StoredCredential -Target "MyApp-Prod" -UserName "svc_myapp" -Password "P@ssw0rd" -Persist LocalMachine
$storedCred = Get-StoredCredential -Target "MyApp-Prod"
Write-Host "从凭据管理器加载:$($storedCred.UserName)"

执行结果示例:

1
2
3
凭据已安全保存:DBAdmin
已加载凭据:admin@example.com
从凭据管理器加载:svc_myapp

输入验证与净化

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
# 路径遍历攻击防护
function Get-SafeFilePath {
param([Parameter(Mandatory)][string]$UserInput)

# 规范化路径
$basePath = "C:\Data\Uploads"
$fullPath = [System.IO.Path]::GetFullPath(
[System.IO.Path]::Combine($basePath, $UserInput)
)

# 检查是否在允许的基目录下
if (-not $fullPath.StartsWith($basePath, [StringComparison]::OrdinalIgnoreCase)) {
throw "非法路径:$UserInput(路径遍历攻击)"
}

return $fullPath
}

# 测试路径遍历
$tests = @("report.csv", "..\..\Windows\System32\config\SAM", "subdir\file.txt")
foreach ($test in $tests) {
try {
$safePath = Get-SafeFilePath -UserInput $test
Write-Host "合法路径:$safePath" -ForegroundColor Green
} catch {
Write-Host "拦截:$test - $($_.Exception.Message)" -ForegroundColor Red
}
}

# SQL 注入防护——始终使用参数化查询
function Get-UserData {
param(
[Parameter(Mandatory)]
[string]$ConnectionString,

[Parameter(Mandatory)]
[string]$UserName
)

# 验证输入格式
if ($UserName -notmatch '^[a-zA-Z0-9_-]{1,50}$') {
throw "非法用户名格式:$UserName"
}

$query = "SELECT Id, Name, Email FROM Users WHERE Name = @userName"

$connection = New-Object System.Data.SqlClient.SqlConnection($ConnectionString)
$command = $connection.CreateCommand()
$command.CommandText = $query

# 使用参数化查询防止 SQL 注入
$param = $command.Parameters.Add("@userName", [System.Data.SqlDbType]::VarChar, 50)
$param.Value = $UserName

try {
$connection.Open()
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($command)
$table = New-Object System.Data.DataTable
$adapter.Fill($table) | Out-Null
return $table
} finally {
$connection.Close()
}
}

# 命令注入防护
function Invoke-SafeCommand {
param([Parameter(Mandatory)][string]$FileName)

# 白名单验证
$allowedExtensions = @('.csv', '.json', '.xml', '.txt')
$ext = [System.IO.Path]::GetExtension($FileName).ToLower()

if ($ext -notin $allowedExtensions) {
throw "不支持的文件类型:$ext"
}

if ($FileName -match '[<>|&]') {
throw "文件名包含非法字符"
}

# 使用参数数组而非字符串拼接
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = "cmd.exe"
$psi.Arguments = "/c type `"$FileName`""
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true

$process = [System.Diagnostics.Process]::Start($psi)
$output = $process.StandardOutput.ReadToEnd()
$process.WaitForExit()

return $output
}

执行结果示例:

1
2
3
合法路径:C:\Data\Uploads\report.csv
拦截:..\..\Windows\System32\config\SAM - 非法路径:..\..\Windows\System32\config\SAM(路径遍历攻击)
合法路径:C:\Data\Uploads\subdir\file.txt

脚本代码签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 获取代码签名证书
$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1

if (-not $cert) {
Write-Host "未找到代码签名证书,创建自签名证书用于测试..." -ForegroundColor Yellow
$cert = New-SelfSignedCertificate -Type CodeSigningCert `
-Subject "CN=PowerShell Script Signing" `
-CertStoreLocation "Cert:\CurrentUser\My"
Write-Host "已创建测试证书:$($cert.Thumbprint)" -ForegroundColor Green
}

# 对脚本进行签名
$scriptPath = "C:\Scripts\Deploy-App.ps1"
Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $cert
Write-Host "脚本已签名:$scriptPath" -ForegroundColor Green

# 验证签名
$signature = Get-AuthenticodeSignature $scriptPath
Write-Host "签名状态:$($signature.Status)"
Write-Host "签名者:$($signature.SignerCertificate.Subject)"

# 设置执行策略
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Write-Host "执行策略已设为 RemoteSigned" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
已创建测试证书:A1B2C3D4E5F6...
脚本已签名:C:\Scripts\Deploy-App.ps1
签名状态:Valid
签名者:CN=PowerShell Script Signing
执行策略已设为 RemoteSigned

审计日志与合规

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
function Write-AuditLog {
<#
.SYNOPSIS
写入安全审计日志
#>
param(
[Parameter(Mandatory)]
[string]$Action,

[string]$Target = "",

[string]$Result = "Success",

[string]$Details = ""
)

$entry = [PSCustomObject]@{
Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
User = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
Computer = $env:COMPUTERNAME
Action = $Action
Target = $Target
Result = $Result
Details = $Details
}

$logDir = "C:\Logs\Audit"
New-Item $logDir -ItemType Directory -Force | Out-Null

$logFile = "$logDir\audit-$(Get-Date -Format 'yyyy-MM').csv"

if (Test-Path $logFile) {
$entry | Export-Csv $logFile -Append -NoTypeInformation -Encoding UTF8
} else {
$entry | Export-Csv $logFile -NoTypeInformation -Encoding UTF8
}
}

# 关键操作自动记录审计日志
function Restart-ServiceWithAudit {
param(
[Parameter(Mandatory)]
[string]$ServiceName,

[string]$Reason
)

Write-AuditLog -Action "ServiceRestart" -Target $ServiceName -Details "原因:$Reason"

try {
Restart-Service -Name $ServiceName -Force -ErrorAction Stop
$svc = Get-Service $ServiceName
Write-AuditLog -Action "ServiceRestartResult" -Target $ServiceName `
-Result "Success" -Details "服务状态:$($svc.Status)"
Write-Host "服务已重启:$ServiceName" -ForegroundColor Green
} catch {
Write-AuditLog -Action "ServiceRestartResult" -Target $ServiceName `
-Result "Failed" -Details $_.Exception.Message
Write-Host "重启失败:$($_.Exception.Message)" -ForegroundColor Red
throw
}
}

Restart-ServiceWithAudit -ServiceName "Spooler" -Reason "打印队列堵塞"

执行结果示例:

1
服务已重启:Spooler

安全基线检查脚本

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
function Test-SecurityBaseline {
<#
.SYNOPSIS
执行基本的安全基线检查
#>

$results = @()

# 检查 1:执行策略
$policy = Get-ExecutionPolicy
$results += [PSCustomObject]@{
Check = "执行策略"
Value = $policy.ToString()
Status = if ($policy -in @('Restricted', 'RemoteSigned', 'AllSigned')) { "PASS" } else { "WARN" }
Details = "当前策略:$policy"
}

# 检查 2:管理员权限
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
$results += [PSCustomObject]@{
Check = "管理员权限"
Value = $isAdmin.ToString()
Status = if (-not $isAdmin) { "PASS" } else { "WARN" }
Details = if ($isAdmin) { "以管理员身份运行,存在提权风险" } else { "以普通用户运行" }
}

# 检查 3:PowerShell 版本
$psVersion = $PSVersionTable.PSVersion.ToString()
$results += [PSCustomObject]@{
Check = "PowerShell 版本"
Value = $psVersion
Status = if ($PSVersionTable.PSVersion.Major -ge 5) { "PASS" } else { "FAIL" }
Details = "版本:$psVersion"
}

# 检查 4:脚本块日志
$sbLogging = Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
-ErrorAction SilentlyContinue
$results += [PSCustomObject]@{
Check = "脚本块日志"
Value = if ($sbLogging.EnableScriptBlockLogging -eq 1) { "启用" } else { "未启用" }
Status = if ($sbLogging.EnableScriptBlockLogging -eq 1) { "PASS" } else { "WARN" }
Details = "建议启用脚本块日志记录"
}

$results | Format-Table -AutoSize

$failCount = ($results | Where-Object { $_.Status -eq "FAIL" }).Count
$warnCount = ($results | Where-Object { $_.Status -eq "WARN" }).Count

Write-Host "`n检查结果:$($results.Count) 项 | PASS: $($results.Count - $failCount - $warnCount) | WARN: $warnCount | FAIL: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } elseif ($warnCount -gt 0) { "Yellow" } else { "Green" })
}

Test-SecurityBaseline

执行结果示例:

1
2
3
4
5
6
7
8
Check           Value    Status Details
----- ----- ------ -------
执行策略 RemoteSigned PASS 当前策略:RemoteSigned
管理员权限 False PASS 以普通用户运行
PowerShell 版本 7.4.2 PASS 版本:7.4.2
脚本块日志 启用 PASS 建议启用脚本块日志记录

检查结果:4 项 | PASS: 4 | WARN: 0 | FAIL: 0

注意事项

  1. 绝不硬编码密码:密码、密钥、Token 等敏感信息应使用凭据管理器、环境变量或密钥库
  2. 最小权限原则:脚本使用最小必要权限运行,避免使用管理员权限执行常规任务
  3. 输入验证:所有外部输入都必须验证和净化,防止注入攻击(SQL、命令、路径遍历)
  4. 传输加密:远程管理使用 HTTPS/SSL,禁用不安全的协议(TLS 1.0/1.1)
  5. 审计追踪:关键操作记录审计日志,包含操作者、时间、目标、结果
  6. 签名策略:生产环境使用 AllSignedRemoteSigned 执行策略,阻止未签名脚本运行

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 密钥和服务账户密码,并在密钥库中同步更新