在故障排查或脚本审查中查看客户的 PowerShell 脚本时,有相当高的概率会遇到这样的东西。$password = "P@ssw0rd123"── 连接文件服务器、基干系统的数据库、发送邮件、Web API 的密钥。由于优先考虑「能跑起来」,密码就以明文形式嵌入脚本中,在共享文件夹或 Git 仓库里存活了好几年。
麻烦的是,写下这行代码的人自己也「明知道这样不好」。那么正确的做法应该是什么呢?SecureString、Export-Clixml、SecretManagement、凭据管理器、Azure Key Vault……选项多到让人眼花缭乱,各自的适用范围与局限也很难分辨清楚。更何况还能听到「SecureString 已经不再被推荐」这样的说法,让人不知道该相信什么。这种混乱是有原因的,只要理清脉络,路径其实非常清晰。
本文面向管理公司内部运维脚本与定期批处理的信息系统・运维负责人,从明文密码究竟有什么问题讲起,按机制逐一梳理 PowerShell 中处理凭据的各种工具,并把「无人值守执行该怎么做」的现实解法整理成判断表。以 PowerShell 7.x 为基准,同时给出面向 Windows PowerShell 5.1 现场的注意事项。
1. 先说结论
- 明文密码的问题在于,只要文件被看到,泄露就已经确定。Git 历史记录、共享文件夹、备份、日志——凡是脚本副本会增加的途径,都会成为泄露途径。即使写了把明文转换为 SecureString 的代码(
ConvertTo-SecureString -AsPlainText),只要原始明文仍然留在脚本或日志里,就不能算是解决了问题。12 - 交互式使用的脚本,基本形式是用 Get-Credential 接收 PSCredential。密码不会显示在屏幕上,可以作为对象直接传给各命令的 -Credential 参数。3
- .NET 官方明确表示「不建议在新开发中使用」SecureString。加密仅在 Windows 上进行,非 Windows 平台内部不会被加密。另一方面,PowerShell 出于兼容性考虑仍在继续使用 SecureString,当前只能把它当作标准的传递格式来相处。不要过度信任它,也不要把它当作自制保护机制的地基。45
- 在无人值守执行中保存凭据文件时,用 Export-Clixml 进行 DPAPI 加密是最小构成的实用解法。因为只有保存时的用户、保存时的机器才能解密,所以即使文件单独泄露也打不开。反过来说,就必须「由任务计划程序的执行账户自身来保存」。在非 Windows 平台上不会被加密。6
- 如果要处理多个秘密,用 SecretManagement + SecretStore 进行统一管理。通过 Set-Secret/Get-Secret 统一的接口,可以把保存位置从本地的 SecretStore 一直替换到 Azure Key Vault。不过在无人值守执行中,保管库密码的处理仍然是一个课题。78
- 最优先应该考虑的是「一开始就不持有凭据」的设计。只要为执行账户(域账户或 gMSA)本身授予连接目标的权限,脚本中的密码就会消失。如果是 gMSA,甚至可以把密码管理本身完全交给操作系统。910
- 要把日志与转录中的泄露也纳入设计考虑。官方文档警告称,一旦启用脚本块日志,脚本中使用的凭据等敏感数据就有可能被写入事件日志。只要在命令行中输入过明文密码,就应当当作它一定会被留下记录来考虑。11
2. 明文的问题所在 ── 泄露途径与副本数量成正比
首先确认一下需要改写的典型模式。
# 反面模式: 无论哪一种,「明文都残留在脚本中」这一点是相同的
$password = "P@ssw0rd123"
# 即使转换为 SecureString,第 1 行写下的原始明文依然存在。
# PSScriptAnalyzer 会把这种 -AsPlainText 的用法检测为错误
$secure = ConvertTo-SecureString $password -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential('svc-transfer', $secure)
直接写入密码之所以危险,并不只是因为「可能被恶意入侵者读到」。更重要的是脚本作为一种成果物的性质——仅仅是在进行正常的运维操作,副本就会不断增殖。
| 副本产生的位置 | 会发生什么 |
|---|---|
| Git 仓库 | 一旦提交过的密码会被永久保存在历史记录中。事后修改文件,历史记录里仍然留有 |
| 共享文件夹 | 扩散给所有拥有读取权限的人,再加上备份与世代副本 |
| 邮件・聊天工具 | 一旦作为「用这个脚本」的附件发出,就再也无法控制 |
| 事件日志 | 如果启用了脚本块日志,执行过的代码内容会被记录到日志中11 |
| 转录(Transcript) | Start-Transcript 或组织的转录设置,会把流经屏幕的内容记录下来11 |
这个结构意味着,明文密码的对策无法通过「不让文件被看到」来实现。无论把访问权限收紧到什么程度,都无法封住 Git 历史记录和备份。把密码转移到脚本之外、放进加密的保管位置才是根本思路。这个想法与关于 .NET 应用配置文件所写的《Windows 应用程序不要把敏感信息以明文存进配置文件的最佳实践》是相同的,原则在 PowerShell 中同样不变。
另外,当场把明文转换为 SecureString 的写法 ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force 并不是解决方案。PSScriptAnalyzer(官方的静态分析工具)会把这种写法检测为错误(Severity: Error)。因为它绕过了加密、把明文暴露在内存中,而且原始明文仍然会一直留在脚本内。1
3. 工具的真实面貌 ── PSCredential、SecureString 及其局限
3.1. Get-Credential 与 PSCredential
交互式脚本的基本形式仅此而已。
# 要求输入用户名和密码,并作为 PSCredential 对象接收
$cred = Get-Credential -Message '请输入基干数据库的连接账户'
# 可以原样传给带有 -Credential 参数的命令
Invoke-Command -ComputerName APPSV01 -Credential $cred -ScriptBlock { hostname }
Get-Credential 会提示输入用户名和密码,并返回一个 PSCredential 对象。在 Windows PowerShell 5.1 中会弹出对话框,在 PowerShell 6 及以后则改为在控制台中输入。3 密码会在 PSCredential 内部以 SecureString 的形式保存,不会显示在屏幕上。在允许「由人当场输入」的运维场景下,不需要做任何更复杂的事情。
编写自定义函数时,应设计为接收 [PSCredential] 类型的 -Credential 参数,而不是用 [string] 接收密码。具体写法的细节,官方说明「Add Credential support to PowerShell functions」中有完整的整理。2
3.2. SecureString 的真实面貌 ── 该如何正确理解「不推荐」
关于 SecureString,有三个应该了解的事实。
- .NET 官方建议「不要在新开发中使用」。内部数组的加密仅在 Windows 上进行,在非 Windows 平台上内部存储不会被加密。而且在实际使用的那一刻,终究还是要转换为明文表示,因此它的效果只是缩短暴露时间。官方推荐的替代方案是「指向进程外保存的凭据的不透明句柄」,也就是操作系统的凭据存储、Key Vault 之类的机制。4
- 即便如此,PowerShell 的标准装备仍然以 SecureString 为前提。PowerShell 出于兼容性考虑一直支持 SecureString,如今仍被用于避免在控制台或日志中意外暴露的用途。它的定位是「比明文字符串更安全」。5
- 可以轻松还原为明文。在 PowerShell 7 中,只需一句
ConvertFrom-SecureString -AsPlainText就能变回明文。12 与其把 SecureString 当作「打不开的保险箱」,不如把它理解为「防止不小心被看到的信封」,这样更符合它的实际情况。
实务上的结论是:不要因为 SecureString 就跑去实现复杂的自制加密。把它当作 PowerShell 工具(Get-Credential、SecretManagement)所要求的格式来使用,保护的主体则放在 DPAPI 或保管库这类进程外的机制上。
4. 无人值守执行的常规做法 ── Export-Clixml 的 DPAPI 保存与「同一用户・同一机器」的高墙
由任务计划程序驱动的无人值守脚本,不能用 Get-Credential 去问人。最小构成的实用解法,就是通过 Export-Clixml 生成凭据文件。
# --- 准备(仅需一次,用任务的执行账户来执行) ---
$cred = Get-Credential -Message '联动用账户'
$cred | Export-Clixml -Path 'D:\Jobs\secrets\transfer.credential'
# --- 正式脚本(无人值守执行) ---
$cred = Import-Clixml -Path 'D:\Jobs\secrets\transfer.credential'
# 例: 用于需要不同凭据的共享连接或远程执行
Invoke-Command -ComputerName FILESV01 -Credential $cred -ScriptBlock { Get-ChildItem D:\Export }
Export-Clixml 会用 Windows 的 DPAPI(数据保护 API)对凭据对象进行加密后保存。只有当保存时使用的用户账户,在保存时所用的那台计算机上打开,才能够解密。导出的文件在其他机器或以其他用户身份都无法使用。613 「即使文件被带走也打不开」这一特性,正是它适合作为无人值守执行保存位置的原因。
至于为什么会这样,只要往机制里深入一层就能理解。DPAPI 会用从用户登录凭据(通常是密码的哈希值)派生出的密钥来加密「主密钥」,并把它放在该用户的配置文件内。数据本体则用从这个主密钥派生出的会话密钥进行加密。也就是说,真正握有密钥的其实是「能够以该用户身份登录」这件事本身,好处是不需要另外把密钥文件保管在别处,代价则是密钥被绑定在用户和机器上。官方文档也说明「通常只有拥有与加密用户相同登录凭据的用户才能解密,加密和解密通常需要在同一台计算机上进行」。14 这就是「仅限同一用户・同一机器」的实质内容。
不过,这种「仅限同一用户・同一机器」既是一种保护,同时也是运维中的一个陷阱。
- 必须与任务计划程序的执行账户保持一致。用自己的账户创建的 .credential 文件,一旦任务改用服务账户运行,就会立即无法解密。准备工作必须「以任务的执行账户身份」进行(用该账户启动 PowerShell 来保存)。任务的执行账户与登录类型的思路,在《任务计划程序的任务不执行、以 0x1 结束 ── 原因排查与安全的运维设计》中有详细说明。
- 服务器更换、账户变更时必然需要重新创建。这是「迁移后就跑不动了」的经典原因,因此应把准备步骤做成脚本并保留在仓库中(当然不包含密码本身)。
- 在同一账户下运行的进程可以解密。DPAPI 对以该用户身份运行的代码是开放的,因此不在执行账户上混装不必要的软件、把账户权限控制到最小,这类周边的卫生管理仍然是必要的。
- 在非 Windows 平台上不会被加密。官方文档明确指出,在 macOS/Linux 上会以实质明文(Unicode 字符数组)的形式输出。6
这里也写明「以该账户启动 PowerShell 来保存」的具体步骤。把上面准备部分(Get-Credential 和 Export-Clixml 这两行)保存为 D:\Jobs\save-credential.ps1,然后以该账户身份启动 PowerShell 来执行它。
# 用任务的执行账户来进行准备: 用 runas 以该账户身份启动 PowerShell
# 密码由 runas 以交互方式询问,因此不会在命令行、历史记录、转录中留下明文
runas /user:CONTOSO\svc-transfer "pwsh.exe -NoProfile -NoExit -File D:\Jobs\save-credential.ps1"
runas 使用的是相当于交互式登录的权限,因此在没有为该账户授予「本地登录」权限的环境中会失败。在这种情况下,现实的做法是仅在准备作业期间允许登录,保存完成后再恢复原来的设置。由于 Get-Credential 本身的输入就需要一个交互式会话,因此「新建一个无人值守的任务来完成准备」这种方法是行不通的。另外,在执行账户的配置文件从未被创建过的机器上,只有经过首次登录创建配置文件之后,DPAPI 的主密钥才会被放置,这也是需要走这道流程的原因之一。
另外,也存在给 ConvertFrom-SecureString 指定 -Key 进行 AES 加密的方法,但这样做往往只是把「如何保护密钥文件」这个同样的问题往后错开了一步而已。12 一旦出现需要跨机器的需求,就应该转向下一章的保管库,或者第 6 章「不持有凭据的设计」。
5. 当秘密逐渐增多时 ── SecretManagement 与 SecretStore
随着连接目标增多,API 密钥和令牌也混杂进来之后,.credential 文件散落各处就会达到管理的极限。这时可以使用 SecretManagement 模块。它是通往秘密保管位置(保管库)的统一接口,实际的保存工作由扩展保管库来承担。除了本地保存的 SecretStore(微软出品)之外,Azure Key Vault、KeePass 等扩展保管库也都可以用同一套命令来处理。715
# 仅需一次: 安装模块并注册保管库
Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore
Register-SecretVault -Name SecretStore -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
# 注册秘密(首次访问时会要求设置保管库密码)
Set-Secret -Name TransferJobCred -Secret (Get-Credential)
# 脚本一侧: 按名称取出。即使保存位置发生变化,这一行也不需要改动
$cred = Get-Secret -Name TransferJobCred
秘密的值除了 PSCredential 和 SecureString 之外,字符串、字节数组、哈希表也都可以保存,首次访问时会要求为保管库本身设置一个保护密码。16 好处在于,脚本不再需要知道「保存在哪里」这件事。开发机上用 SecretStore、生产环境改用 Azure Key Vault(由 Az.KeyVault 模块提供扩展保管库)这样的切换,只需修改 Register-SecretVault 的配置即可完成。177
另一方面,在把它引入无人值守执行时,有三点需要注意。
- SecretStore 默认会以交互方式要求提供保管库密码。直接原样放到任务计划程序上会因为等待提示而卡住。官方文档针对无人值守执行给出的方案是:把 Interaction 设为
None,把避险保存到 DPAPI 保护文件(Export-Clixml)中的保管库密码,通过 Unlock-SecretStore 传入解锁。8 也就是说,保管库的钥匙最终仍然要靠 DPAPI 来守护,因此继承了前一章「同一用户・同一机器」的限制。也可以把密码要求本身完全禁用(Authentication None),但这样一来密钥就只由文件系统权限来保护,对于需要强保护的用途,官方并不推荐这种做法。18 - 在 gMSA 等托管账户下无法运行。SecretManagement 依赖用户配置文件(
$env:LOCALAPPDATA)和 DPAPI,官方明确指出目前不支持没有配置文件的 Windows 托管账户。15 如果希望让运行在 gMSA 下的作业持有秘密,就不能选择这套组合(不过换个角度看,既然用了 gMSA,往往本来就更适合转向不持有秘密的设计——见下一章)。 - SecretManagement/SecretStore 已被视为功能完备(feature complete),不再积极开发新功能。安全修复和重大缺陷的处理仍会持续,因此使用本身没有问题,但官方也表示「秘密的形态正在向无密码或联合凭据转变」,在长期设计中也应把重新审视认证方式本身(例如 Entra ID 的托管标识)纳入视野。7 托管标识是一种用分配给 Azure 资源的身份获取令牌、从而在不持有密码或密钥的情况下向支持 Entra ID 的服务进行身份验证的机制。至于接下来该读什么,可以先在概述页面掌握系统分配和用户分配的区别,19 如果要从 PowerShell 中使用,以 Az 模块的
Connect-AzAccount -Identity(以托管标识登录)作为入口就不会走弯路。
也存在把 Windows 的凭据管理器(Credential Manager)当作保管库使用的社区扩展。7 用 cmdkey 注册的凭据会被自动使用等凭据管理器自身的行为,正如在《网络驱动器与 UNC 路径的陷阱 ── 业务应用中处理文件服务器(共享文件夹)的实务》中提到的那样,它同样也是以用户配置文件为单位的,这一点是相同的。
6. 最优先的现实解法 ── 一开始就不持有凭据
到这里为止,我们一直在堆叠「如何安全地保存」这个问题的解法,但实际上无人值守执行最干净利落的答案,并不在于保存方式的巧思,而是让脚本从一开始就不需要持有凭据。
Windows 的任务或服务是在执行账户的安全上下文中运行的。如果连接目标是由 Windows 身份验证保护的——共享文件夹的 ACL、SQL Server 的 Windows 身份验证、公司内部 API 的 Windows 集成身份验证——只要为执行账户本身授予连接目标的权限,脚本中就完全不会出现密码,也不需要 Get-Secret。身份验证会由 Kerberos 凭借执行账户的身份自动完成。
支撑这套构成的是 gMSA(组托管服务账户)。gMSA 是域中的一种托管账户,密码管理由 Windows 操作系统来承担。9 密码是 240 字节的随机生成值,由操作系统每 30 天自动更换一次,因此没有任何人知道密码,也不会因为过期而导致停机。10 而且 gMSA 不仅可以用于 Windows 服务,也可以用于任务计划程序的任务。20
这里也写出 gMSA 导入步骤的骨架。因为如果只停留在「可以用」,读者不知道接下来该查什么。前提条件是:域・林的功能级别为 Windows Server 2012 以上,操作账户拥有相当于 Domain Admins 的权限,如果在域控制器以外的机器上操作,则需要安装 RSAT(ActiveDirectory 模块)。20
# 1) 每个域只需一次: 创建 KDS 根密钥(如果已经存在则不需要这一步)
# 需要等待复制到全部域控制器,因此实际能够创建 gMSA 之前最长可能需要 10 小时
Add-KdsRootKey -EffectiveImmediately
# 2) 创建 gMSA。PrincipalsAllowedToRetrieveManagedPassword 用于
# 指定允许获取密码的计算机(汇总成的安全组)
New-ADServiceAccount -Name svc-transfer -DNSHostName svc-transfer.contoso.local `
-PrincipalsAllowedToRetrieveManagedPassword 'gMSA-Hosts'
# 3) 在实际运行作业的服务器一侧: 安装 gMSA,并确认能否获取密码
Install-ADServiceAccount -Identity svc-transfer
Test-ADServiceAccount -Identity svc-transfer
创建 KDS 根密钥是每个域只需进行一次的作业。刚创建完成后,由于复制方面的原因暂时还无法创建 gMSA;如果在验证环境中不想等待,官方给出了用 -EffectiveTime ((Get-Date).AddHours(-10)) 指定过去时间的步骤(请不要在生产环境中使用)。21 创建之后,可以通过检查 KDS 服务运行日志中是否记录了事件 ID 4004 来进行确认。20
分配给任务时,需要在任务的执行账户中指定 gMSA(形如 CONTOSO\svc-transfer$ 这种末尾带 $ 的账户名)。由于 gMSA 的密码没有人知道,因此和以「输入执行账户密码」为前提的 GUI 步骤操作方式不同。实务中会用 ScheduledTasks 模块的 New-ScheduledTaskPrincipal 和 Register-ScheduledTask 来注册,请把这两个 cmdlet 的文档作为接下来的查阅方向。此外,有时还需要在该服务器上为 gMSA 设置「作为批处理作业登录」的权限。
把判断的优先顺序整理出来,就是这样。
- 如果连接目标可以使用 Windows 身份验证,就通过为执行账户(域账户/gMSA)授予权限来消除凭据。域环境中的无人值守作业应首先考虑这一点。9
- 只有对做不到这一点的对象(SQL 身份验证的数据库、API 密钥、工作组内的 NAS 等)才保存秘密。单个的话用 Export-Clixml 的 DPAPI 保存,多个的话用 SecretManagement + SecretStore。68
- 对于连接云端资源,优先使用托管标识/联合凭据,而不是保存密钥。即便需要保存,也应保存到 Azure Key Vault 这样的专用保管库中。177
在需要向远程服务器明确传递凭据来执行处理的场景中,还会涉及 PowerShell Remoting 的身份验证机制。请参阅同期发布的《PowerShell Remoting(WinRM)入门 ── 批量管理多台 Windows》。
7. 不让秘密残留在日志・转录中
即使把保存方式做得再牢固,如果从执行时的记录中泄露出去,也是毫无意义的。需要把握的要点有两个。
第一,PowerShell 具备多种记录执行内容的功能,有可能已经被组织的策略设置为启用状态。启用脚本块日志后,处理过的所有脚本块内容都会被记录到事件日志中。官方文档明确警告「启用脚本日志后,脚本中使用的凭据等敏感数据有可能被写入事件日志」,并建议在诊断以外的用途中并用保护的事件日志(Protected Event Logging)。11 保护的事件日志是一种把目标事件日志的内容用 CMS(加密消息语法)的公钥加密后写入、只有在拥有私钥的另一处安全位置才能解密的机制。11 转录(操作的书写记录)也是同样的道理,流经控制台的内容会被保留在文件中。
可以通过策略写入的注册表值,来确认自己环境的实际状态。保护的事件日志由组策略中「计算机配置 > 管理模板 > Windows 组件 > 事件日志记录(Event Logging)」下的「启用保护的事件日志(Enable Protected Event Logging)」进行设置,对应的注册表如下所示。22
# 保护的事件日志是否已启用(如果策略未配置,该键本身就不存在)
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\EventLog\ProtectedEventLogging' `
-Name EnableProtectedEventLogging -ErrorAction SilentlyContinue
# 脚本块日志一侧的策略(PowerShell 7 系)
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\ScriptBlockLogging' `
-Name EnableScriptBlockLogging -ErrorAction SilentlyContinue
# Windows PowerShell 5.1 一侧则是另一个键
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' `
-Name EnableScriptBlockLogging -ErrorAction SilentlyContinue
脚本块日志的注册表值分为两处:PowerShell 7 系位于 PowerShellCore 之下11,Windows PowerShell 5.1 位于 Windows\PowerShell 之下23。在 5.1 与 7 共存的服务器上,请两者都检查一遍。
确认时的关键点在于——只启用了脚本块日志、却没有启用保护的事件日志——这种组合。这是一种「有记录,但以明文形式积存」的状态,请在部署涉及凭据的脚本之前先掌握清楚。
第二,正因如此,要彻底贯彻「明文不经过命令行或屏幕」的写法。
- 不要设计成用参数接收密码。只要输入过
.\job.ps1 -Password "P@ssw0rd",就应当当作它一定会残留在日志、历史记录、进程列表中来考虑。如果一定要接收,就用 PSCredential 或 SecureString 来接收。2
# 自制脚本入口的规范: 不用 [string] 接收密码
[CmdletBinding()]
param(
# 凭据用 PSCredential 接收。省略时通过 Get-Credential 交互式获取,
# 在无人值守执行中则传入 Import-Clixml 或 Get-Secret 的结果
[Parameter(Mandatory)]
[System.Management.Automation.PSCredential]$Credential
)
# 包含秘密的变量不进行调试输出。最多只输出到用户名为止
Write-Verbose "连接账户: $($Credential.UserName)"
- 不要用 Write-Host 或 Write-Verbose 对包含秘密的变量进行调试输出。排查问题时「以为只是临时」的输出,会被永久保留在转录中。
- 注意不要混入异常消息中。先拼装好连接字符串再抛出错误,很容易导致 catch 到的日志把连接字符串也一并记录下来。关于错误处理中日志该写什么的设计,也请参阅《PowerShell 的错误处理与重试设计 ── 从 try/catch 失效的陷阱到 exit code、重试的实务定式》。
8. 实务定式(判断表)
| 情况 | 推荐 | 判断标准 |
|---|---|---|
| 由人当场执行 | Get-Credential | 不保存是最安全的做法。用 PSCredential 接收后传给 -Credential3 |
| 域内的无人值守作业(连接目标使用 Windows 身份验证) | 为执行账户/gMSA 授予权限 | 最优先考虑不持有凭据的设计。gMSA 在任务计划程序中也可以使用920 |
| 无人值守作业中秘密只有 1~2 个 | Export-Clixml 的 DPAPI 保存 | 由任务的执行账户自身来保存。机器・账户变更时需要重新创建6 |
| 无人值守作业中秘密数量多・未来想更换保存位置 | SecretManagement + SecretStore | 保管库密码通过 DPAPI 保存 + Unlock-SecretStore 供给。gMSA 下无法使用815 |
| 云端资源・多台服务器共用的秘密 | Azure Key Vault(+SecretManagement) | 局限于单机的 DPAPI 无法共享。当需要集中管理与审计时选用17 |
| 脚本内的明文 + ConvertTo-SecureString | 应改写的对象 | PSScriptAnalyzer 判定为错误的糟糕写法。应迁移到以上任意一种方案1 |
如果拿不定主意,请按照「不持有 > 固定在机器上持有(DPAPI) > 用保管库持有」的顺序,从上往下依次考虑。
9. 总结
- 明文密码的本质问题在于,Git 历史记录、共享文件夹、日志这些副本增殖的途径,全都会变成泄露途径。仅靠访问权限管理是守不住的。
- 交互式执行用 Get-Credential + PSCredential 就足够了。请不要编写用 [string] 接收密码的自定义函数。
- SecureString 虽被 .NET 官方列为新开发不推荐使用,但仍然作为 PowerShell 的标准传递格式存在。请把它当作「防止不小心暴露的信封」来看待,保护的主体应放在外部机制上。
- Export-Clixml 的 DPAPI 保存「仅限同一用户・同一机器」。要点在于由任务计划程序的执行账户自身来创建保存文件,在非 Windows 平台上不会被加密。
- SecretManagement + SecretStore 对多个秘密的统一管理很有效,但在无人值守执行中需要设计好保管库密码的供给方式,并且在 gMSA 下无法使用。采用时也要考虑到它已被视为功能完备这一点。
- 最优先的方案是不持有凭据的设计。请首先考虑能否通过 Windows 身份验证加上为执行账户(gMSA)授予权限,把密码从脚本中彻底消除。
- 脚本块日志和转录中都有可能残留敏感数据。要彻底贯彻让明文不经过命令行、屏幕、异常消息的写法。
相关文章
- Windows 应用程序不要把敏感信息以明文存进配置文件的最佳实践
- 任务计划程序的任务不执行、以 0x1 结束 ── 原因排查与安全的运维设计
- 网络驱动器与 UNC 路径的陷阱 ── 业务应用中处理文件服务器(共享文件夹)的实务
- PowerShell Remoting(WinRM)入门 ── 批量管理多台 Windows
- PowerShell 的错误处理与重试设计 ── 从 try/catch 失效的陷阱到 exit code、重试的实务定式
- PowerShell 脚本的参数设计与模块化 ── 从「能运行的脚本」到「可以交给别人的脚本」
相关咨询领域
合同会社小村软件承接运维脚本・定期批处理中嵌入凭据的清查与向安全保管的迁移、包括 gMSA 在内的执行账户设计、无人值守作业的安全评审。欢迎从「因为在运行就不敢碰」而被搁置至今的明文密码整理开始咨询。
参考链接
-
Microsoft Learn,AvoidUsingConvertToSecureStringWithPlainText。关于 PSScriptAnalyzer 会把 ConvertTo-SecureString 的 -AsPlainText 用法检测为错误(Severity: Error)、这种写法绕过加密把敏感信息以明文暴露在内存中、以及官方列举 Read-Host -AsSecureString 与 SecretStore 模块作为替代方案的说明。 ↩ ↩2 ↩3
-
Microsoft Learn,Add Credential support to PowerShell functions。关于为自定义函数添加 PSCredential 参数的方法,以及明文密码会被记录到各种日志中、因而在使用 ConvertTo-SecureString -AsPlainText 时会发出警告的原因的说明。 ↩ ↩2 ↩3
-
Microsoft Learn,Get-Credential。关于 Get-Credential 会提示输入用户名和密码并返回 PSCredential 对象、Windows PowerShell 5.1 中会弹出对话框・PowerShell 6 及以后在控制台中提示输入、以及将其传给带有 -Credential 参数的命令使用的说明。 ↩ ↩2 ↩3
-
Microsoft Learn,SecureString Class。关于官方建议在 .NET(Core) 的新开发中不要使用 SecureString、加密仅在 Windows 上进行而非 Windows 平台内部存储不会被加密、使用时需要转换为明文表示、以及推荐的替代方案是指向进程外保存的凭据的不透明句柄的说明。 ↩ ↩2
-
Microsoft Learn,Advisory Development Guidelines。关于 .NET 不建议新使用 SecureString,而 PowerShell 出于向后兼容考虑仍持续支持 SecureString、它比明文字符串更安全并被用于避免在控制台或日志中意外暴露、以及由于可以轻易转换为明文因此应谨慎使用的说明。 ↩ ↩2
-
Microsoft Learn,Export-Clixml。关于 Export-Clixml 会用 Windows 的 DPAPI 对凭据对象加密后保存、只有保存时的用户账户、保存时的计算机上才能解密、导出文件无法在其他机器或以其他用户身份使用,以及在非 Windows(macOS/Linux)上不会被加密、实质上以明文输出的说明。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn,Overview of the SecretManagement and SecretStore modules。关于 SecretManagement 是通往扩展保管库的统一接口、SecretStore 是把内容加密保存到本地文件的跨平台扩展保管库、存在 Azure Key Vault・KeePass・凭据管理器(CredMan)等扩展保管库,以及 Secret 系列模块被视为功能完备、已停止积极开发、仅继续提供安全修复的说明。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn,Use the SecretStore in automation。关于针对无人值守执行把 SecretStore 的 Interaction 配置为 None、把保管库密码保存到用 Export-Clixml 进行 DPAPI 加密的文件中并用 Unlock-SecretStore 解锁的构成,以及这是一种仅限 Windows 的解决方案的说明。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,Group Managed Service Accounts overview。关于 gMSA 是一种提供自动密码管理与简化 SPN 管理的托管域账户、密码管理由 Windows 操作系统而非管理员来承担的说明。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,Secure group managed service accounts。关于 gMSA 的密码是 240 字节的随机生成值、由操作系统每 30 天自动更换、因而不再需要规划密码变更或服务停机,以及官方推荐把 gMSA 用作本地部署服务的账户类型的说明。 ↩ ↩2
-
Microsoft Learn,about_Logging_Windows。关于启用脚本块日志后 PowerShell 处理的全部脚本块内容都会被记录到事件日志中、提高日志级别后脚本中使用的凭据等敏感数据有可能被包含在日志中、在 PowerShell 7 系中启用脚本块日志的注册表值为
HKLM:\Software\Policies\Microsoft\PowerShellCore\ScriptBlockLogging下的EnableScriptBlockLogging,以及 Protected Event Logging 是一种用 CMS(加密消息语法)的公钥加密事件日志内容、并在持有私钥的安全位置进行解密的机制的说明。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn,ConvertFrom-SecureString。关于可以把 SecureString 转换为加密的标准字符串、未指定密钥时使用 Windows 的 DPAPI・指定 Key/SecureKey 时使用 AES、以及可以用 -AsPlainText 直接转换为明文字符串的说明。 ↩ ↩2
-
Microsoft Learn,Import-Clixml。关于可以用 Import-Clixml 还原 Export-Clixml 保存的凭据・安全字符串,从而避免在脚本中写入明文密码的风险的说明。 ↩
-
Microsoft Learn,CryptProtectData function。关于通常只有拥有与加密用户相同登录凭据的用户才能解密、加密与解密通常需要在同一台计算机上进行,以及该函数会根据用户的登录凭据生成会话密钥来执行加密的说明。 ↩
-
Microsoft Learn,Understanding the SecretManagement module。关于 SecretManagement 是通过已注册的扩展保管库来保存・获取秘密的机制、保管库的注册是按用户上下文划分的,以及由于依赖 $env:LOCALAPPDATA 和 DPAPI,目前在 Windows 的托管账户(managed accounts)下无法运行的说明。 ↩ ↩2 ↩3
-
Microsoft Learn,Get started with the SecretStore module。关于用 Register-SecretVault 注册 SecretStore、Set-Secret/Get-Secret/Get-SecretInfo 的基本操作,以及首次访问时会要求设置保管库密码的说明。 ↩
-
Microsoft Learn,Use Azure Key Vault in automation。关于 Az.KeyVault 3.3.0 及以后版本包含 SecretManagement 扩展、可以用 Register-SecretVault 把 Azure Key Vault 注册为 SecretManagement 的保管库、并通过 Get-Secret 等命令进行操作的说明。 ↩ ↩2 ↩3
-
Microsoft Learn,Understanding the security features of SecretManagement and SecretStore。关于完全禁用密码验证时,解密密钥就只由文件系统权限来保护,因此不推荐用于需要强安全保护的系统的说明。 ↩
-
Microsoft Learn,What are managed identities for Azure resources?。关于托管标识是一种利用 Entra ID 上的身份、在不管理凭据的情况下获取令牌的机制,以及系统分配和用户分配这两种类型及其区别的说明。 ↩
-
Microsoft Learn,Manage group Managed Service Accounts。关于 gMSA 可以用于服务控制管理器配置的服务、IIS 应用程序池、任务计划程序的任务,前提条件(域・林的功能级别为 Windows Server 2012 以上、Domain Admins/Enterprise Admins 的成员身份、KDS 根密钥的存在以及通过 KdsSvc 运行日志的事件 ID 4004 进行确认、在域控制器以外的机器上操作需要 RSAT),用 New-ADServiceAccount 创建的步骤与 PrincipalsAllowedToRetrieveManagedPassword 的指定,以及用 Install-ADServiceAccount/Test-ADServiceAccount 进行部署与确认的说明。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,Create the Key Distribution Services KDS Root Key。关于 gMSA 的密码生成需要 KDS 根密钥、用 Add-KdsRootKey -EffectiveImmediately 创建、以及为等待复制收敛到全部域控制器最长需要等待 10 小时的规范,还有针对验证环境用 Add-KdsRootKey -EffectiveTime 指定过去时间来避开等待时间的步骤的说明。 ↩
-
Microsoft Learn,ADMX_EventLogging Policy CSP。关于 EnableProtectedEventLogging 策略位于计算机配置的「Windows 组件 > 事件日志记录」下,对应的注册表键为
Software\Policies\Microsoft\Windows\EventLog\ProtectedEventLogging、值名为EnableProtectedEventLogging的说明。 ↩ -
Microsoft Learn,about_Logging (Windows PowerShell 5.1)。关于在 Windows PowerShell 5.1 中启用脚本块日志的注册表值为
HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging下的EnableScriptBlockLogging的说明。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
PowerShell Remoting(WinRM)入门 ── 批量管理多台 Windows
PowerShell Remoting(WinRM)批量管理多台 Windows 的入门指南。整理其原理与端口 5985/5986、Enable-PSRemoting 会做的事情、工作组环境的 TrustedHosts、Invoke-Command 与 PSSession、...
PowerShell 的执行策略与脚本签名 ── 从「用 Bypass 蒙混过关」的运维方式毕业的实务指南
PowerShell 的执行策略『不是安全边界,而是一种安全装置』。本文梳理 RemoteSigned 等策略的差异、作用域的优先顺序、Mark of the Web 与 Unblock-File、脚本签名,以及公司内部分发的现实运维方式。
PowerShell脚本运行慢时该看哪里 ── 数组・管道・匹配的诀窍
整理 PowerShell 脚本变慢的常见原因。从数组 += 导致 O(n^2) 的原理、管道与 foreach 的差异、匹配的哈希表化、文件 I/O 的改善,到正确的测量方法,从实务角度进行讲解。
不再使用 Write-Host ── PowerShell 的输出流与日志设计
本文整理 PowerShell 六个输出流的用法区分、Write-Host 存在的问题与正确的使用场景、函数返回值被污染的原因、通过 -Verbose 与 -InformationVariable 实现调用方控制,以及结构化日志的留存方法。
PowerShell 的并行处理 ── ForEach-Object -Parallel 与作业(Job)的选用之道
本文从实务角度整理 ForEach-Object -Parallel、Start-ThreadJob、Start-Job 的区别与选用之道,$using: 与线程安全性,ThrottleLimit 的确定方法,以及反而会变慢的情形。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- 为什么不能在 PowerShell 脚本中以明文写入密码?
- 因为脚本天生就是会被复制、共享、并留存在历史记录中的产物。一旦提交到 Git,就很难再从历史记录中彻底清除;放到共享文件夹,则拥有读取权限的所有人都能看到;而脚本块日志和转录(transcript)会把命令内容原样记录下来。也就是说,明文密码会造成「只要文件被看到,就等同于泄露确定」的结构。对策的根本思路不是先堵住泄露途径,而是把密码转移到脚本之外受保护的位置。
- 用 Export-Clixml 保存的凭据到底有多安全?
- 在 Windows 上会通过 DPAPI(数据保护 API)进行加密,只有保存时使用的用户账户、在同一台计算机上才能解密。即使文件被窃取,在其他机器或以其他用户身份也无法打开,因此作为无人值守执行的保存位置是一个实用的选择。不过它并非万能:在同一账户下运行的进程同样可以解密,此外在 macOS 或 Linux 上不会被加密,实质上会以明文形式输出,这一点需要注意。如果要从任务计划程序中使用,就必须由任务的执行账户自身来创建保存文件。
- 现在还应该使用 SecureString 吗?
- .NET 官方文档明确指出,不建议在新开发中使用 SecureString。加密仅在 Windows 上进行,在非 Windows 平台上内部不会被加密。而且在实际使用的那一刻,终究还是要还原为明文。另一方面,在 PowerShell 的世界里,Get-Credential、SecretManagement 等标准工具都以 SecureString 为前提,比起用明文字符串到处传递,暴露的机会要少得多,因此现实的做法是暂时把它当作「PowerShell 的标准传递格式」来对待。请不要把它当作自制加密机制的素材。
- 在任务计划程序的无人值守执行中使用 SecretStore,会不会因为要求输入密码而卡住?
- 在保持默认配置的情况下会卡住。因为 SecretStore 默认会要求提供保管库密码,并弹出交互式提示。针对无人值守执行,官方文档给出的方案是:将 Interaction 设为 None,把保管库密码保存到 DPAPI 保护的文件(Export-Clixml)中,再用 Unlock-SecretStore 读取该文件来解锁。不过这本质上是「用 DPAPI 保护保管库的钥匙」这种构成,最终仍然继承了 DPAPI 的限制(同一用户・同一台机器)。在此之前,请先考虑能否通过 gMSA 或为执行账户授予权限,从根本上消除对凭据本身的需要。
- 有没有一开始就不保存凭据的方法?
- 有,而且这正是首选方案。Windows 的任务或服务是以执行账户的权限运行的,因此只要为连接目标(共享文件夹、SQL Server 的 Windows 身份验证等)授予执行账户自身的权限,脚本就完全不需要持有密码。如果把执行账户设为 gMSA(组托管服务账户),密码就会是 240 字节的随机值,由操作系统每 30 天自动更新一次,可以做到没有任何人知道密码。gMSA 同样也能用于任务计划程序的任务。