PowerShell 中安全處理認證資訊 ── 把明文密碼逐出腳本

· · PowerShell, Windows, 資訊安全, 認證資訊, DPAPI, 自動化, 維運改善, 腳本

在進行故障排查或審閱腳本時查看客戶的 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,第一行寫的原始明文依然存在。
# 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 歷史紀錄與備份。把密碼移出腳本,放到經過加密的保存位置才是根本做法。這個想法與筆者在「Windows 應用程式不要把機密資訊以明文存進設定檔的最佳實踐」一文中,針對 .NET 應用程式設定檔所寫的內容相同,在 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 的形式保存,不會顯示在畫面上。在允許「由人員當場輸入」這種運作方式的場景中,沒有必要做更複雜的事。

在自行撰寫函式時,應該設計成不用 [string] 接收密碼,而是接收 [PSCredential] 的 -Credential 參數。詳細寫法整理在官方說明「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 以互動方式詢問,因此不會在命令列、歷史紀錄、逐字稿(transcript)中留下明文
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(Active Directory 模組)。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 服務的運作記錄中是否記錄了事件識別碼 4004 來確認。20

分配給工作時,要在工作的執行帳戶指定 gMSA(帳戶名稱結尾為$,例如CONTOSO\svc-transfer$)。由於 gMSA 沒有任何人知道密碼,因此和以「輸入執行帳戶密碼」為前提的 GUI 步驟做法不同。實務上會透過 ScheduledTasks 模組的 New-ScheduledTaskPrincipal 與 Register-ScheduledTask 進行登錄,請把這兩個 Cmdlet 的文件列為下一步要查閱的資料。同時,在該伺服器上,有時也需要額外設定授予 gMSA「以批次工作登入」的權利。

把判斷的優先順序整理一下,會是這樣。

  1. 如果連線對象可以採用 Windows 驗證,就透過對執行帳戶(網域帳戶/gMSA)授予權限,消除認證資訊。在網域環境中的無人執行工作,應優先考慮這個做法。9
  2. 只針對做不到這一點的對象(採用 SQL 驗證的資料庫、API 金鑰、工作群組的 NAS 等)才保存秘密。單一秘密就用 Export-Clixml 的 DPAPI 保存,數量較多就用 SecretManagement + SecretStore。68
  3. 連線到雲端資源時,優先使用受控識別/聯合認證資訊,而非保存金鑰。即使需要保存,也應保存到 Azure Key Vault 這類專用保存庫中。177

在需要向遠端伺服器明確傳遞認證資訊來執行處理的場景中,也會牽涉到 PowerShell Remoting 的驗證機制。請參閱同步發布的「PowerShell Remoting/WinRM 入門」。

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 的錯誤處理與重試設計」。

8. 實務上的標準做法(判斷表)

情境 建議 判斷依據
人員在現場當下執行 Get-Credential 不儲存是最安全的做法。以 PSCredential 接收後傳給 -Credential3
網域內的無人執行工作(連線對象採用 Windows 驗證) 對執行帳戶/gMSA 授予權限 最優先考量不持有認證資訊的設計。gMSA 也能用在工作排程器中920
無人執行工作、秘密只有 1~2 個 以 Export-Clixml 進行 DPAPI 保存 由工作的執行帳戶自行保存。伺服器或帳戶變更時需要重新製作6
無人執行工作、秘密數量多,且未來想更換儲存位置 SecretManagement + SecretStore 保存庫密碼透過 DPAPI 保存 + Unlock-SecretStore 供應。無法用於 gMSA815
雲端資源、多台伺服器共用的秘密 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)授予權限,讓密碼從腳本中消失。
  • 腳本區塊記錄與逐字稿中可能殘留敏感資料。請徹底貫徹不讓明文經過命令列、畫面、例外訊息的寫法。

相關文章

相關諮詢領域

合同會社小村軟體承接維運腳本、定期批次作業中嵌入的認證資訊盤點與遷移至安全保存方式、包含 gMSA 在內的執行帳戶設計,以及無人執行工作的安全性審查。歡迎從整理「因為還能動就不敢碰」而被擱置至今的明文密碼開始洽詢。

參考連結

  1. Microsoft Learn, AvoidUsingConvertToSecureStringWithPlainText. 關於 PSScriptAnalyzer 會把 ConvertTo-SecureString 的 -AsPlainText 用法判定為錯誤(Severity: Error)、這種寫法會繞過加密把敏感資訊以明文暴露在記憶體中,以及官方列出 Read-Host -AsSecureString 或 SecretStore 模組作為替代方案的說明。  2 3

  2. Microsoft Learn, Add Credential support to PowerShell functions. 關於在自訂函式中新增 PSCredential 參數的方法,以及因為明文密碼會被記錄到各種日誌中,所以在使用 ConvertTo-SecureString -AsPlainText 時會出現警告的原因的說明。  2 3

  3. Microsoft Learn, Get-Credential. 關於 Get-Credential 會提示輸入使用者名稱與密碼並回傳 PSCredential 物件、在 Windows PowerShell 5.1 中以對話方塊・PowerShell 6 以後以主控台方式要求輸入、以及傳給具有 -Credential 參數的命令使用的說明。  2 3

  4. Microsoft Learn, SecureString Class. 關於 .NET(Core)建議在新開發中不要使用 SecureString、加密只會在 Windows 上進行而非 Windows 平台的內部儲存不會加密、使用時需要轉換為明文表示、以及建議的替代方案是指向處理序外部所保存認證資訊的不透明控制代碼的說明。  2

  5. Microsoft Learn, Advisory Development Guidelines. 關於 .NET 雖不建議新用途使用 SecureString,但 PowerShell 為了向下相容仍持續支援 SecureString、它比明文字串安全並用於避免偶然暴露在主控台或日誌中、以及因為容易轉換回明文所以使用時應多加留意的說明。  2

  6. Microsoft Learn, Export-Clixml. 關於 Export-Clixml 會用 Windows 的 DPAPI 將認證資訊物件加密後保存、只有保存時的使用者帳戶在保存時的那台電腦上才能解密、匯出的檔案無法在其他機器或其他使用者身上使用,以及在非 Windows(macOS/Linux)平台不會加密、實質以明文輸出的說明。  2 3 4 5

  7. Microsoft Learn, Overview of the SecretManagement and SecretStore modules. 關於 SecretManagement 是通往擴充保存庫的統一介面、SecretStore 是把資料加密保存到本機檔案的跨平台擴充保存庫、存在 Azure Key Vault・KeePass・認證管理員(CredMan)等擴充保存庫,以及 Secret 系列模組被視為功能完成、已結束積極開發、僅持續進行安全性修正的說明。  2 3 4 5 6

  8. Microsoft Learn, Use the SecretStore in automation. 關於針對無人執行將 SecretStore 的 Interaction 組態為 None、把保存庫密碼保存到以 Export-Clixml 進行 DPAPI 加密的檔案中並透過 Unlock-SecretStore 解鎖的組態,以及這是僅限 Windows 的解決方案的說明。  2 3 4

  9. Microsoft Learn, Group Managed Service Accounts overview. 關於 gMSA 是提供自動密碼管理與簡化 SPN 管理的受控網域帳戶,密碼管理由 Windows 作業系統而非管理員負責的說明。  2 3 4

  10. Microsoft Learn, Secure group managed service accounts. 關於 gMSA 的密碼是 240 位元組的隨機產生值、作業系統每 30 天自動變更一次因此不需要規劃密碼變更或讓服務停機,以及建議將 gMSA 作為地端服務帳戶類型使用的說明。  2

  11. Microsoft Learn, about_Logging_Windows. 關於啟用腳本區塊記錄後 PowerShell 處理的所有腳本區塊內容都會被記錄到事件記錄中、提高記錄層級後腳本中使用過的認證資訊等敏感資料可能會被包含在記錄中、在 PowerShell 7 系列中啟用腳本區塊記錄的登錄值為HKLM:\Software\Policies\Microsoft\PowerShellCore\ScriptBlockLogging下的EnableScriptBlockLogging,以及 Protected Event Logging 是以 CMS(加密訊息語法)的公開金鑰將事件記錄的內容加密、並在持有私密金鑰的安全位置解密的機制的說明。  2 3 4 5 6

  12. Microsoft Learn, ConvertFrom-SecureString. 關於可以把 SecureString 轉換成加密後的標準字串、未指定金鑰時使用 Windows 的 DPAPI・指定 Key/SecureKey 時使用 AES、以及可以用 -AsPlainText 直接轉換成明文字串的說明。  2

  13. Microsoft Learn, Import-Clixml. 關於可以用 Import-Clixml 還原 Export-Clixml 所保存的認證資訊・安全字串,藉此避免在腳本中寫入明文密碼之風險的說明。 

  14. Microsoft Learn, CryptProtectData function. 關於通常只有與加密時相同登入認證資訊的使用者才能解密、加密與解密通常必須在同一台電腦上進行、以及此函式會從使用者的登入認證資訊產生工作階段金鑰來進行加密的說明。 

  15. Microsoft Learn, Understanding the SecretManagement module. 關於 SecretManagement 是透過已註冊的擴充保存庫來保存・取得秘密的機制、保存庫的註冊是以使用者內容為單位、以及因為依賴 $env:LOCALAPPDATA 與 DPAPI,目前無法在 Windows 受控帳戶(managed accounts)上運作的說明。  2 3

  16. Microsoft Learn, Get started with the SecretStore module. 關於用 Register-SecretVault 註冊 SecretStore、Set-Secret/Get-Secret/Get-SecretInfo 的基本操作,以及首次存取時會被要求設定保存庫密碼的說明。 

  17. Microsoft Learn, Use Azure Key Vault in automation. 關於 Az.KeyVault 3.3.0 以後版本包含 SecretManagement 擴充功能,可以透過 Register-SecretVault 把 Azure Key Vault 註冊為 SecretManagement 的保存庫,再用 Get-Secret 等命令操作的說明。  2 3

  18. Microsoft Learn, Understanding the security features of SecretManagement and SecretStore. 關於若完全停用密碼驗證,解密金鑰就只靠檔案系統權限保護,因此不建議用於需要高強度安全保護的系統的說明。 

  19. Microsoft Learn, What are managed identities for Azure resources?. 關於受控識別是利用 Entra ID 上的識別,在不管理認證資訊的情況下取得權杖的機制,以及系統指派與使用者指派這兩種類型及其差異的說明。 

  20. Microsoft Learn, Manage group Managed Service Accounts. 關於 gMSA 可以用在服務控制管理員組態的服務、IIS 應用程式集區、工作排程器的工作中,前提條件(網域・樹系的功能等級須為 Windows Server 2012 以上、須具備 Domain Admins/Enterprise Admins 成員資格、須存在 KDS 根金鑰並可透過 KdsSvc 運作記錄的事件識別碼 4004 確認、在網域控制站以外的機器作業需要 RSAT)、以 New-ADServiceAccount 建立的步驟與 PrincipalsAllowedToRetrieveManagedPassword 的指定,以及透過 Install-ADServiceAccount/Test-ADServiceAccount 進行導入與確認的說明。  2 3 4

  21. Microsoft Learn, Create the Key Distribution Services KDS Root Key. 關於 gMSA 的密碼產生需要 KDS 根金鑰、用 Add-KdsRootKey -EffectiveImmediately 建立、以及因為要等待複寫收斂到所有網域控制站最長需等待 10 小時的規格,還有針對驗證環境用 Add-KdsRootKey -EffectiveTime 指定過去時間以避開等待時間的步驟的說明。 

  22. Microsoft Learn, ADMX_EventLogging Policy CSP. 關於 EnableProtectedEventLogging 原則位於電腦設定的「Windows 元件 > 事件記錄」中、對應的登錄機碼為Software\Policies\Microsoft\Windows\EventLog\ProtectedEventLogging、值名稱為EnableProtectedEventLogging的說明。 

  23. Microsoft Learn, about_Logging (Windows PowerShell 5.1). 關於在 Windows PowerShell 5.1 中啟用腳本區塊記錄的登錄值為HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging下的EnableScriptBlockLogging的說明。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

為什麼不可以在 PowerShell 腳本中以明文寫入密碼?
因為腳本本來就是一種預設會被複製、共享、留存在歷史紀錄中的產出物。一旦提交到 Git,就很難從歷史紀錄中消除;放到共用資料夾,所有具有讀取權限的人都能看到;腳本區塊記錄或逐字稿也會把命令的內容原封不動地記錄下來。也就是說,明文密碼會製造出「只要檔案被看見,外洩就已成定局」的結構。與其先想著封鎖外洩途徑,把密碼移到腳本之外經過保護的位置,才是對策的根本方向。
用 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 也能用在工作排程器的工作上。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽