應用程式不應在設定檔中要求密碼、API 金鑰或權杖。
設定描述行為。它應存放在 git、程式碼審查、錯誤報告,以及開發人員的機器中。
機密授予權限。它需要受限的存取權限與獨立的輪替機制。
將兩者放在同一個檔案中,會將不同的生命週期與受眾耦合在一起。如果輪替密碼需要重新產生應用程式設定,表示介面已過度耦合。
NixOS 包含 110 種因應措施
Section titled “NixOS contains 110 workarounds for this”
我們審核了 nixpkgs 在 commit 141f212 時,所有處理真實機密的 445 個 NixOS 模組,並依據機密值的最終去向進行分類。
| 機密值的最終去向 | 模組數 | 比例 |
|---|---|---|
| 在執行時合併到設定檔中 | 110 | 25% |
內嵌到 /nix/store 中的設定檔 | 42 | 9% |
| 以環境變數形式傳遞 | 161 | 36% |
| 留在應用程式開啟的獨立檔案中 | 58 | 13% |
| 透過 systemd credentials 載入 | 53 | 12% |
| 作為命令列引數傳遞 | 19 | 4% |
| 分類不明確 | 2 | — |
值得關注的數字是 110。有四分之一的模組安全地取得機密,卻因為應用程式只接受這種介面,而將機密複製到設定中。
這些模組使用 envsubst、replace-secret、jq、yq、sed 或自訂程式碼,在啟動時組裝受限制的檔案。結果可能安全,但每個模組都必須負責應用程式專屬且具安全敏感性的膠水程式碼,只為了將兩個本應分開的輸入結合在一起。
這並非 NixOS 特有。相同的因應措施也會以 entrypoint 腳本、Helm template、init container 或 CI 插值步驟的形式出現在其他平台上。
附帶一提,有 42 個模組會將機密內嵌到全域可讀的 /nix/store 中。這個直接的安全問題已在 nixpkgs issue #24288 中追蹤。而這 110 個執行時合併的案例則說明更廣泛的問題:即使部署作者避免了外洩,缺少分離仍會產生額外工作。
為機密提供專屬介面
Section titled “Give secrets their own interface”
應用程式應透過專屬的執行階段通道接受機密值,例如:
password_file或token_file設定;- systemd credential;
- 範圍明確的環境變數;
- 或外部機密提供者。
這些機制並非同等安全:環境變數可能被繼承、引數可能出現在程序清單中,而檔案仍需正確的權限。分離所保證的是,部署者不必再製造另一個帶有機密的設定版本。
原則很簡單,但在各種環境中實作並不容易。本機開發可能使用系統金鑰環、CI 可能使用環境變數,而正式環境可能使用 1Password 或 Vault。若沒有共同的抽象層,每個環境都需要自己的命名、查詢、驗證與注入膠水程式碼。
我在 Cachix 中犯下的錯誤
Section titled “How I got it wrong in Cachix”
Cachix 過去將其驗證權杖與各快取的簽署金鑰儲存在 ~/.config/cachix/cachix.dhall,與快取名稱及其他設定放在一起。這樣很方便,但即使檔案中大部分是普通設定,仍必須將整個檔案視為機密。
典型的檔案會直接混合兩者:
{ authToken = "XXX-AUTH-TOKEN"
, binaryCaches =
[ { name = "mycache"
, secretKey = "XXX-SIGNING-KEY"
}
]
}
快取名稱是設定;驗證權杖與簽署金鑰則是機密。你無法分享快取設定而不同時分享憑證。
devenv 2.2 透過 SecretSpec 將權杖分離。專案宣告 CACHIX_AUTH_TOKEN,devenv 從設定的提供者解析它,並將值傳遞給 Cachix,而不會加入 devenv 的設定中。
Cachix PR #737 透過 SecretSpec Haskell SDK 將相同的邊界帶入用戶端。它從 SecretSpec 解析 CACHIX_AUTH_TOKEN 與 CACHIX_SIGNING_KEY,並可將它們儲存在使用者選擇的提供者中,而非 cachix.dhall。現有的環境變數與設定檔仍保留較高的優先順序以維持相容性。此 PR 仍在開放中,尚未包含在已發行的 Cachix 版本中。
這正是 SecretSpec 所要解決的問題:設定宣告需求,而每個環境選擇值的存放位置。
宣告一次,隨處解析
Section titled “Declare once, resolve anywhere”
SecretSpec 透過讓 secretspec.toml 成為應用程式所需內容的宣告(而不儲存值)來實現這種分離:
[project]
name = "myapp"
[profiles.production]
DATABASE_URL = { description = "Postgres connection string" }
STRIPE_API_KEY = { description = "Stripe secret key" }
Providers 決定值的存放位置。開發者可以使用系統金鑰環、CI 可以使用環境變數,而正式環境可以使用 1Password、Vault/OpenBao 或雲端機密管理員,而無需更改宣告。
現有應用程式可以在啟動時接收解析後的值:
secretspec run -- ./myapp
應用程式也可以透過 SecretSpec SDKs 直接解析,支援 Rust、Python、Go、Ruby、Node.js/TypeScript、Haskell、PHP 與 C#,所有語言共用相同的解析器,讓行為在各語言間保持一致。
Providers 負責機密值的來源。SDKs 為應用程式提供慣用的取用方式。設定則維持為所需內容的可分享宣告。
讓邊界更實用
Section titled “Making the boundary practical”
如果你維護應用程式,請停止將密碼與權杖加入普通設定結構。改為接受檔案參照、credential、環境變數或提供者。
對於 NixOS,SecretSpec issue #65 追蹤如何透過官方整合宣告與解析機密,而無需每個模組都實作替換膠水程式碼。
在開發人員機器、CI 與正式環境之間保持一致的機密處理,過去需要只有專門的平台團隊才能建置的基礎架構。任何規模的專案都應該能夠在不必先建立自己的機密平台的情況下,將機密與設定分離。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.