在 NixOS 上使用 sops-nix 進行機密管理
原文由 Michael Stapelberg 于 發布,訂閱此部落格
密碼與各種機密資訊,像是加密金鑰檔案,在運算環境中無所不在。設定 Linux 系統時,遲早都得在某個地方放入密碼——舉例來說,當我將現有的 Linux 網路儲存(NAS)環境遷移到 NixOS時,就必須在 NixOS 設定檔中指定想要的 Samba 密碼(或是在 NixOS 之外手動管理)。如果是個人電腦,這樣做沒什麼問題,但如果目標是分享系統設定(例如放在 Git 儲存庫中),就需要不同的解法:機密管理。
什麼是機密管理?
機密管理系統背後的基本想法,是將靜態的機密資訊加密,也就是說,即使有人複製了包含你 NixOS 系統設定的 Git 儲存庫,也無法存取(因此也無法部署)那些已加密的機密。
從概念上來說,我們需要做到:
- 將機密加密,讓目標系統能夠解密。
- 將機密加密,讓其他協作此設定的人也能解密。
- 讓目標系統在執行期間解密機密。
- 告訴我們的軟體要去哪裡取得解密後的機密。
sops-nix 設定
在本文中,我將示範如何使用 sops-nix 達成上述目標。以下先快速介紹我們會用到的三個組成部分:
- sops 是一個能在 Git 中以加密形式對機密進行版本控管的工具。
- sops 能在新增或移除授權金鑰時,輕鬆地重新加密這些機密。
- sops 非常有彈性,能與眾多其他工具/供應商搭配使用。
- sops-nix 提供了將 sops 與 Nix/NixOS 整合的方法
- 將 sops 與
age(1)搭配使用,就能直接使用現有的 SSH 私鑰(人)或 SSH 主機私鑰(機器),而不必另外管理一組金鑰檔案。
你可能會好奇,為什麼我選擇 sops-nix 而不是另一個選項 agenix?當初我第一次研究時,覺得 sops-nix 的設定說明比較容易理解,而且我也想保留以其他方式使用 sops 的彈性,而不只是搭配 age。如果你對 agenix 感興趣,可以看看 Andreas Gohr 關於 agenix 的部落格文章。
步驟 1. 準備工作
以下操作是我在一台已安裝 Nix 工具並啟用 Nix Flakes 的 Arch Linux 機器上執行的。連結中有針對 Debian 或 Fedora 等其他系統的說明,也請參考該連結。
步驟 2. 從個人 SSH 金鑰取得 age 身分
我不想另外管理一個金鑰檔案,因此會使用 ssh-to-age 從我已妥善備份的 SSH 私鑰檔案衍生出金鑰:
midna % mkdir -p $HOME/.config/sops/age/
midna % read -s SSH_TO_AGE_PASSPHRASE; export SSH_TO_AGE_PASSPHRASE
midna % nix run nixpkgs#ssh-to-age -- \
-private-key \
-i $HOME/.ssh/id_ed25519 \
-o $HOME/.config/sops/age/keys.txt(SSH_TO_AGE_PASSPHRASE 選項記載於 ssh-to-age README 中。)
要顯示這個 age 身分(私鑰)所對應的 age 接收者(公鑰),我執行了:
midna % nix shell nixpkgs#age
midna 2 % age-keygen -y $HOME/.config/sops/age/keys.txt
age10e9tt2qwq90y5hvl35dau0sm5cm4qvegtw2a70v7sz5fy99de42s9d5nkf步驟 3. 為遠端機器取得 age 接收者
同樣地,我會從遠端系統的 SSH 主機金鑰衍生出 age 接收者:
batchn % cat /etc/ssh/ssh_host_ed25519_key.pub | nix run nixpkgs#ssh-to-age
age1wnwfnrqhewjh39pmtyc8zhqw606znskt4h5p9s3pve4apd67gapqj6tr0k步驟 4. 為你的 Git 儲存庫設定 sops
在我的 Git 儲存庫(nix-configs)中,每個 NixOS 系統各有一個子目錄,例如 tree(1) 的顯示結果如下:
├── batchn
│ ├── configuration.nix
│ ├── disk-config.nix
│ ├── flake.lock
│ ├── flake.nix
│ ├── hardware-configuration.nix
│ ├── Makefile
│ ├── secrets
│ │ └── example.yaml
├── wiki
│ ├── configuration.nix
│ ├── disk-config.nix
│ ├── flake.lock
│ ├── flake.nix
│ ├── hardware-configuration.nix
│ ├── Makefile
…在 Git 儲存庫的根目錄(與 batchn 目錄同層)中,我建立了 .sops.yaml,內容如下:
keys:
- &admin_michael age10e9tt2qwq90y5hvl35dau0sm5cm4qvegtw2a70v7sz5fy99de42s9d5nkf
- &server_batchn age1wnwfnrqhewjh39pmtyc8zhqw606znskt4h5p9s3pve4apd67gapqj6tr0k
# …more server keys go here…
creation_rules:
- path_regex: batchn/secrets/[^/]+\.(yaml|json|env|ini)$
key_groups:
- age:
- *admin_michael
- *server_batchn我管理的系統越多,就需要設定越多的 keys 和 creation_rules。
建立規則(creation rules)會告訴 sops 加密檔案時該使用哪些金鑰。在我的設定中,通常每個系統只使用單一檔案,但如果想針對系統的某一部分與他人協作,我也可以想像將部分機密拆到另一個檔案中。
步驟 5. 使用 sops 管理機密
既然已經告訴 sops 要為哪些接收者加密,現在就可以執行以下指令,在已設定的編輯器中解密並編輯 secrets/example.yaml:
midna ~/nix-configs/batchn % nix run nixpkgs#sops secrets/example.yaml最簡單的金鑰檔案只包含一個鍵,例如:
api-key: hello world :)儲存並離開編輯器後,sops 就會更新已加密的 secrets/example.yaml。
步驟 6. 在 NixOS 中設定 sops
接下來,我們需要在 NixOS 中參照這個加密檔案,並啟用 sops-nix 整合,讓解密後的機密能在系統上使用。
在 flake.nix 中,我在 inputs 區段加入了 sops-nix,並加入了 NixOS 模組。我展示完整的 diff,是因為這些行所在的位置,和內容本身同樣重要:
--- c/batchn/flake.nix
+++ i/batchn/flake.nix
@@ -1,85 +1,93 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
disko.url = "github:nix-community/disko";
# Use the same version as nixpkgs
disko.inputs.nixpkgs.follows = "nixpkgs";
stapelbergnix.url = "github:stapelberg/nix";
zkjnastools.url = "github:stapelberg/zkj-nas-tools";
+ sops-nix = {
+ url = "github:Mic92/sops-nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+
};
outputs =
{
nixpkgs,
disko,
stapelbergnix,
zkjnastools,
+ sops-nix,
...
}:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = false;
};
in
{
nixosConfigurations.batchn = nixpkgs.lib.nixosSystem {
inherit system;
inherit pkgs;
modules = [
disko.nixosModules.disko
./configuration.nix
stapelbergnix.lib.userSettings
# Use systemd for network configuration
stapelbergnix.lib.systemdNetwork
# Use systemd-boot as bootloader
stapelbergnix.lib.systemdBoot
# Run prometheus node exporter in tailnet
stapelbergnix.lib.prometheusNode
zkjnastools.nixosModules.zkjbackup
+ sops-nix.nixosModules.sops
];
};
formatter.${system} = pkgs.nixfmt-tree;
};
}接著,在 configuration.nix 中,我們告訴 sops-nix 要使用 SSH 主機金鑰作為身分、sops 要去哪裡找到我們的機密,以及 sops-nix 應該在遠端系統上實現哪些機密:
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
sops.defaultSopsFile = ./secrets/example.yaml;
sops.secrets."api-key" = { };部署完成後,就能在運行中的系統上存取機密:
batchn ~ % sudo cat /run/secrets/api-key
hello world :) %
batchn ~ %當然,即使重新啟動機器,機密依然可用,無需重新部署:
batchn ~ % uptime
22:09:23 up 0:00, 1 user, load average: 0,32, 0,08, 0,03
batchn ~ % sudo cat /run/secrets/api-key
hello world :) %
batchn ~ %使用範例
現在機密已經以檔案形式儲存在 /run/secrets 底下,我們要如何使用這些機密呢?
以下幾個小節會展示幾種常見的用法。
使用範例:命令列參數(ExecStart 包裝)
假設你在 NixOS 上已將一個自訂的 Go 伺服器部署為 systemd 服務,如下所示,而你現在想開始管理透過 -securecookie_hash_key 和 -securecookie_block_key 命令列參數傳入的明文機密:
{
users.groups.fortuneserver = { };
users.users.fortuneserver = {
isSystemUser = true;
group = "fortuneserver";
};
systemd.services.fortuneserver = {
description = "fortuneserver";
documentation = [ "https://michael.stapelberg.ch" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "fortuneserver";
Group = "fortuneserver";
ExecStart = ''
"${pkgs.fortuneserver}/bin/fortuneserver" \
-securecookie_hash_key="some-secret-key" \
-securecookie_block_key="a-different-secret-key"
'';
};
};
}搭配以下 sops 機密:
fortuneserver:
securecookie_hash_key: some-secret-key
securecookie_block_key: a-different-secret-key……我們需要調整 NixOS 設定,讓它在執行期間讀取這些機密檔案。由於 ExecStart 指令是由 systemd 直接解析,而不會經過 shell,因此我們使用writeShellScript 輔助函式,然後直接用 cat 讀取檔案:
{
sops.secrets."fortuneserver/securecookie_hash_key" = {
owner = "fortuneserver";
restartUnits = [ "fortuneserver.service" ];
};
sops.secrets."fortuneserver/securecookie_block_key" = {
owner = "fortuneserver";
restartUnits = [ "fortuneserver.service" ];
};
users.groups.fortuneserver = { };
users.users.fortuneserver = {
isSystemUser = true;
group = "fortuneserver";
};
systemd.services.fortuneserver = {
description = "fortuneserver";
documentation = [ "https://michael.stapelberg.ch" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "fortuneserver";
Group = "fortuneserver";
ExecStart = pkgs.writeShellScript "fortuneserver-execstart" ''
"${pkgs.fortuneserver}/bin/fortuneserver" \
-securecookie_hash_key="$(cat /run/secrets/fortuneserver/securecookie_hash_key)" \
-securecookie_block_key="$(cat /run/secrets/fortuneserver/securecookie_block_key)"
'';
};
};
}使用範例:環境變數檔案
如果相關服務不是使用命令列參數,而是用環境變數來設定機密,該怎麼做?我們可以將環境變數檔案放入由 sops 管理的機密中:
translate-fe:
env: |
DEEPL_AUTH_KEY=my-deepl-key……然後讓 systemd 從這個機密檔案套用這些環境變數:
{
sops.secrets."translate-fe/env" = {
owner = "translatefe";
restartUnits = [ "translate-fe.service" ];
};
systemd.services.translate-fe = {
documentation = [ "https://michael.stapelberg.ch" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "translatefe";
EnvironmentFile = [ config.sops.secrets."translate-fe/env".path ];
ExecStart = "${translatefeExecstart}/bin/translate-fe";
};
};
}如果你設定的是 NixOS 模組(而非自行宣告服務),選項名稱不一定叫 EnvironmentFile。例如,對於 oauth2-proxy 服務,你需要設定services.oauth2-proxy.keyFile 選項:
services.oauth2-proxy = {
keyFile = config.sops.secrets."oauth2-proxy/env".path;
enable = true;
# …
};使用範例:systemd 憑證
在前面的範例中,我們將每個機密的擁有者設為執行該服務的使用者帳號。但如果服務使用了 systemd 的 DynamicUser 功能,根本沒有這樣一個使用者帳號,該怎麼辦?
我們可以使用 systemd 的 LoadCredential 功能!例如,我是這樣將 SMTP 密碼提供給 Prometheus Alertmanager 的:
{
sops.secrets."alertmanager/smtp_pw" = {
restartUnits = [ "alertmanager.service" ];
};
systemd.services.alertmanager.serviceConfig.LoadCredential = [
"smtp_pw:${config.sops.secrets."alertmanager/smtp_pw".path}"
];
services.prometheus.alertmanager = {
enable = true;
configuration = {
global = {
smtp_smarthost = "smtp.gmail.com:587";
smtp_from = "[email protected]";
smtp_auth_username = "[email protected]";
smtp_auth_password_file = "/run/credentials/alertmanager.service/smtp_pw";
};
# …remaining config goes here…
};
};
}使用範例:Samba 使用者/密碼
在我的部落格文章〈Migrating my NAS from CoreOS/Flatcar Linux to NixOS〉中,我說明了如何透過 ExecStartPre shell 腳本(與前面介紹的技巧非常類似)來設定 Samba 使用者與密碼(來自 sops 管理的機密)。
結論
對我來說,將機密作為各自加密的檔案放在設定儲存庫中,是很合理的做法!
在我看來,age 能直接搭配 SSH 金鑰使用,讓整個設定變得非常方便。為目標系統的 SSH 主機金鑰加密機密,感覺非常優雅。
希望上述範例足以讓你有效率地在 NixOS 中設定機密!
隨機一篇部落格
留言
登入後參與討論