使用 home‑manager 在 NixOS 中管理 sops‑nix secrets 的 git‑safe 方式
Source: Dev.to
当你对 NixOS 配置(或任何 dotfiles)进行版本控制时,整个系统就可以被复现。
然而,存放在这些文件中的 API 密钥或密码会以 明文 形式保存在仓库里。
- 你不能仅仅用
.gitignore忽略它们——NixOS 在构建/激活时需要这些值。 - 你也不想把它们暴露为环境变量。
解决方案: 使用 sops‑nix——用 SOPS 加密秘密并提交加密后的文件。
在激活时,sops‑nix 会使用仅存在于你机器上的密钥进行解密(仓库中没有这些密钥)。
这样你的 dotfiles 仍然是完全可复现、可推送且不泄漏的。
概览
- 第一个需要管理的密钥:
EXA_API_KEY(用于 Exa.ai)。 - 本指南展示了使用 age 密钥(从你的 SSH 密钥派生)进行 单机 设置(基于 Michael Stapelberg 的方法并进行了修复)。
- 可选:将自己添加为受信任用户,以消除构建警告。
# configuration.nix
nix.settings.trusted-users = [ "@wheel" "noor" ];
重新构建一次:
sudo nixos-rebuild switch
1️⃣ 准备 Age 密钥存储
# Create the directory where sops will look for age keys
mkdir -p ~/.config/sops/age/
进入一个包含所需工具的临时 Nix shell:
nix shell nixpkgs#ssh-to-age nixpkgs#age
1.1 从你的 SSH 私钥创建 age 身份
# If your SSH key is passphrase‑protected, you’ll be prompted for it.
ssh-to-age -private-key -i ~/.ssh/id_ed25519 -o ~/.config/sops/age/keys.txt
锁定权限(强烈推荐):
chmod 600 ~/.config/sops/age/keys.txt
1.2 获取你的 个人 age 接收者
age-keygen -y ~/.config/sops/age/keys.txt
# → prints something like: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxx
复制打印出的值——稍后会用到。
1.3 确认系统 SSH 主机密钥已存在
ls /etc/ssh/ssh_host_ed25519_key # should exist
如果 不存在,生成所有主机密钥:
sudo ssh-keygen -A
1.4 获取 系统 age 接收者(从主机密钥派生)
cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age
# → prints something like: age1yyyyyyyyyyyyyyyyyyyyyyyyyyyy
同样复制此值。
退出临时 shell:
exit
现在你拥有 两个接收者:
| 接收者 | 示例值 |
|---|---|
admin(个人) | age1xxxxx… |
system(主机) | age1yyyyy… |
2️⃣ 创建 .sops.yaml 配置
在你的 dotfiles 仓库根目录创建文件(例如 ~/nixos-dotfiles/.sops.yaml):
# .sops.yaml
keys:
- &admin age1xxxxx... # Your personal age recipient
- &system age1yyyyy... # Your system's SSH host key
creation_rules:
- path_regex: secrets/[^/]+\.yaml$
key_groups:
- age:
- *admin
- *system
这段配置的作用
- 个人密钥(
&admin) – 让你以普通用户身份解密/编辑机密(无需sudo,也不需要重复输入 SSH 密码)。 - 系统密钥(
&system) – 让系统在启动时自动解密机密,并将其暴露在/run/secrets/下。
⚠️ 注意事项: 如果你更换/重新生成 SSH 主机密钥(
/etc/ssh/ssh_host_ed25519_key),必须使用 新的 主机接收者重新加密所有机密,否则启动时的解密将会失败。
3️⃣ 创建并加密你的第一个密钥
cd ~/nixos-dotfiles
mkdir -p secrets
使用 SOPS 打开文件(nix run 包装器会拉取正确的版本):
nix run nixpkgs#sops -- secrets/secrets.yaml
当编辑器打开时,添加密钥:
EXA_API_KEY: "your_actual_api_key_here"
保存并退出。文件现在已加密。
验证加密:
cat secrets/secrets.yaml
# → you’ll see values starting with ENC[AES256_GCM,...]
只有你(使用 ~/.config/sops/age/keys.txt)可以解密它。
以后编辑/查看:
nix run nixpkgs#sops -- secrets/secrets.yaml
4️⃣ 将 sops‑nix 接入你的 NixOS 配置
4.1 将 sops‑nix 添加为 Flake 输入
# flake.nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, home-manager, sops-nix }:
let
system = "x86_64-linux";
in {
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
sops-nix.nixosModules.sops # **Important:** `sops-nix.homeManagerModules.sops` 必须被导入,以便在 `home.nix` 中可见 `sops` 选项。
];
};
};
}
4.2 在 home.nix 中配置密钥
# home.nix
{ config, ... }:
{
# 告诉 sops‑nix 你的 age 私钥所在位置
sops.age.keyFile = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
# 为该用户设置默认的 SOPS 文件
sops.defaultSopsFile = ./secrets/secrets.yaml;
# 声明我们想要公开的密钥
sops.secrets.EXA_API_KEY = { };
# 导出一个指向解密后文件的会话变量
home.sessionVariables = {
EXA_API_KEY = "${config.sops.secrets.EXA_API_KEY.path}";
};
}
你将得到的效果
- 登录时,
EXA_API_KEY指向位于/run/user/<uid>/secrets/EXA_API_KEY的临时文件。 - 该文件中包含 原始密钥值。
- 在 shell 中读取它:
cat "$EXA_API_KEY"。
注意:
sops‑nix将密钥存储为 文件,而不是原始的环境变量。能够从文件路径读取密钥的程序可以直接使用。
5️⃣ 使用密钥
# In a shell
echo "My key is: $(cat "$EXA_API_KEY")"
如果程序需要直接在环境变量中获取原始值,你可以这样包装:
export EXA_API_KEY=$(cat "$EXA_API_KEY")
my-program --api-key "$EXA_API_KEY"
回顾
| 步骤 | 你做了什么 |
|---|---|
| 1️⃣ | 生成了一个来自你的 SSH 密钥的 age 身份,并收集了个人和系统接收者。 |
| 2️⃣ | 创建了 .sops.yaml,它告诉 SOPS 哪些接收者可以加密/解密 secrets/ 下的文件。 |
| 3️⃣ | 将 EXA_API_KEY 添加到 secrets/secrets.yaml 并使用 SOPS 加密。 |
| 4️⃣ | 将 sops‑nix 添加到你的 flake,启用了该模块用于 NixOS 和 Home Manager,并指向你的密钥文件和秘密文件。 |
| 5️⃣ | 导出了指向已解密秘密文件的会话变量,准备使用。 |
现在你的 NixOS 配置(或任何 dotfiles 仓库)可以安全地推送到公共或共享的 Git 远程,永不泄露秘密。 🎉
Source: …
使用 sops‑nix 添加 API Key
1. 为什么需要这一步?
API_KEY 不是 文件路径。要让期望读取文件的程序正常工作,你有两种选择:
- 配置程序 使其从文件读取(许多程序支持
*_FILE环境变量)。 - 包装/启动程序,让它把文件内容读取到环境变量中。
重新构建后,关闭所有已有的终端窗口——已打开的 shell 不会看到新的变量。打开一个全新的终端即可获取更新后的环境。
2. 系统级 sops 配置
在 ~/nixos-dotfiles/configuration.nix 中添加以下内容(注意 NixOS 模块需要外层 { … }: 包装器):
{ ... }:
{
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
}
这段配置的作用
- 声明系统级 SSH 主机密钥,供 NixOS 的
sops模块在解密时使用。 - 必须放在
configuration.nix(NixOS 层),不能放在home.nix。 - Home‑Manager 的
sops模块会使用它自己的sops.age.keyFile(在第 4 步中设置)。 - 使得在启动时无需用户交互即可解密,并为以后系统级的 secret 做准备。
3. 重建并应用
sudo nixos-rebuild switch --flake ~/nixos-dotfiles#nixos
重建后的选项
| 选项 | 命令 | 备注 |
|---|---|---|
| A – 重启(最简单) | sudo reboot | 确保所有服务都能看到新的环境变量。 |
| B – 重启服务 | bash\nsystemctl --user daemon-reload\nsystemctl --user restart sops-nix.service\n | 如果服务之前未激活(home‑manager 问题),可能无效。 |
4. 验证 secret
打开一个 新 终端并运行:
echo $EXA_API_KEY
# → 应该会打印类似 /run/user/1000/secrets/EXA_API_KEY 的路径
cat $EXA_API_KEY
# → 打印实际的 API‑key 值
5. 编辑加密的 secrets 文件
cd ~/nixos-dotfiles
nix run nixpkgs#sops -- secrets/secrets.yaml
完成修改后,保存、提交并重建。
git add flake.nix configuration.nix home.nix .sops.yaml secrets/secrets.yaml
git commit -m "Add EXA_API_KEY secret with sops-nix"
git push
重要提示:
- 你的 age 私钥(
~/.config/sops/age/keys.txt)永远不要提交——它应当保留在仓库之外。- 加密文件(
secrets/secrets.yaml)可以安全提交,因为没有私钥它是无用的。
6. 我们做了什么(总结)
- 生成了 age 密钥,使用现有的 SSH 主机密钥(
ssh-to-age),并将其存放在~/.config/sops/age/keys.txt(未跟踪)。 - 配置
.sops.yaml,包含两个接收者:个人 age 密钥(用于编辑)和系统 SSH 主机密钥(用于启动时解密)。 - 创建 加密的 secrets 文件(
secrets/secrets.yaml),使用sops完成。 - 将
sops‑nix接入 flake:在configuration.nix中加入 NixOS 模块,在home.nix中加入 Home‑Manager 模块。 - 暴露 secret 为环境变量
$EXA_API_KEY,该变量指向解密后位于/run/user/1000/secrets/EXA_API_KEY的文件。
加密文件可以安全推送;私钥永远留在本机。(在后续文章中,我会展示如何在 Kubernetes 集群中通过 YAML 文件使用 sops。)