agenix - 用于 NixOS 的 age](https://github.com/FiloSottile/age) 加密密钥
agenix 是一个小巧便捷的 Nix 库,用于使用常见的公钥-私钥 SSH 密钥对安全地管理和部署密钥:
你可以在源机器上使用多个 SSH 公钥加密一个密钥(密码、访问令牌等),
并将该加密密钥部署到任何拥有其中一个公钥对应私钥的目标机器上。
本项目包含两个部分:
- 一个
agenix命令行应用(CLI),用于将密钥加密为安全的.age文件,这些文件可以复制到 Nix 存储中。 - 一个
agenixNixOS 模块,用于便捷地- 将这些加密密钥(
.age文件)添加到 Nix 存储中,以便使用nixos-rebuild或类似工具像部署其他 Nix 包一样部署它们。 - 在目标机器上使用该机器上的 SSH 私钥自动解密
- 自动将这些解密的密钥挂载到众所周知的路径(如
/run/agenix/...)以供使用。
- 将这些加密密钥(
目录
问题与解决方案
Nix 存储中的所有文件均可被任何系统用户读取,因此它不适合作为存放明文机密的位置。许多现有工具(如 NixOps 的 deployment.keys)将机密与 nixos-rebuild 分开部署,这使得部署、缓存和审计变得更加困难。带外机密管理也降低了可复现性。
agenix 通过使用您现有的 SSH 密钥基础设施和 age 将机密加密到 Nix 存储中来解决这些问题。机密在 NixOS 系统激活期间使用 SSH 主机私钥进行解密。
功能
- 机密使用 SSH 密钥进行加密
- 通过
ssh-keyscan使用系统公钥 - 可以使用 GitHub 上可用的用户公钥(例如,https://github.com/ryantm.keys)
- 通过
- 不使用 GPG
- 代码量很少,因此应该很容易供您审计
- 加密后的机密存储在 Nix 存储中,因此不需要单独的分发机制
注意事项
- 受密码保护的 ssh 密钥:由于 age 不支持 ssh-agent,受密码保护的 ssh 密钥无法正常工作。例如,如果您需要重新加密 20 个机密,您将不得不输入 20 次密码。
安装
通过 niv
首先将其添加到 niv:
$ niv add ryantm/agenix
通过 niv 安装模块
然后在 imports 列表中的 configuration.nix 添加以下内容:
{
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age.nix" ];
}
通过 niv 安装 home-manager 模块
将以下内容添加到你的 home 配置中:
{
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age-home.nix" ];
}
通过 niv 安装 CLI
要安装 agenix 二进制文件:
{
environment.systemPackages = [ (pkgs.callPackage "${(import ./nix/sources.nix).agenix}/pkgs/agenix.nix" {}) ];
}
通过 nix-channel 安装
以 root 身份运行:
$ sudo nix-channel --add https://github.com/ryantm/agenix/archive/main.tar.gz agenix
$ sudo nix-channel --update
通过 nix-channel 安装模块
然后在 imports 列表中,将以下内容添加到你的 configuration.nix:
{
imports = [ <agenix/modules/age.nix> ];
}
通过 nix-channel 安装 home-manager 模块
将以下内容添加到你的 home 配置中:
{
imports = [ <agenix/modules/age-home.nix> ];
}
通过 nix-channel 安装 CLI
要安装 agenix 二进制文件:
{
environment.systemPackages = [ (pkgs.callPackage <agenix/pkgs/agenix.nix> {}) ];
}
通过 fetchTarball 安装
通过 fetchTarball 安装模块
将以下内容添加到你的 configuration.nix 中:
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age.nix" ];
}
或使用固定版本:
{
imports = let
# replace this with an actual commit id or tag
commit = "298b235f664f925b433614dc33380f0662adfc3f";
in [
"${builtins.fetchTarball {
url = "https://github.com/ryantm/agenix/archive/${commit}.tar.gz";
# update hash from nix build output
sha256 = "";
}}/modules/age.nix"
];
}
通过 fetchTarball 安装 home-manager 模块
将以下内容添加到你的 home 配置中:
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age-home.nix" ];
}
或者使用固定版本:
{
imports = let
# replace this with an actual commit id or tag
commit = "298b235f664f925b433614dc33380f0662adfc3f";
in [
"${builtins.fetchTarball {
url = "https://github.com/ryantm/agenix/archive/${commit}.tar.gz";
# update hash from nix build output
sha256 = "";
}}/modules/age-home.nix"
];
}
通过 fetchTarball 安装 CLI
要安装 agenix 二进制文件:
{
environment.systemPackages = [ (pkgs.callPackage "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/pkgs/agenix.nix" {}) ];
}
通过 Flakes 安装
通过 Flakes 安装模块
{
inputs.agenix.url = "github:ryantm/agenix";
# optional, not necessary for the module
#inputs.agenix.inputs.nixpkgs.follows = "nixpkgs";
# optionally choose not to download darwin deps (saves some resources on Linux)
#inputs.agenix.inputs.darwin.follows = "";
outputs = { self, nixpkgs, agenix }: {
# change `yourhostname` to your actual hostname
nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
# change to your system:
system = "x86_64-linux";
modules = [
./configuration.nix
agenix.nixosModules.default
];
};
};
}
通过 Flakes 安装 home-manager 模块
{
inputs.agenix.url = "github:ryantm/agenix";
outputs = { self, nixpkgs, agenix, home-manager }: {
homeConfigurations."username" = home-manager.lib.homeManagerConfiguration {
# ...
modules = [
agenix.homeManagerModules.default
# ...
];
};
};
}
通过 Flakes 安装 CLI
你可以临时运行 CLI 工具而无需安装它:
nix run github:ryantm/agenix -- --help
但你也可以将其永久添加到 NixOS 模块 (将系统 "x86_64-linux" 替换为你的系统):
{
environment.systemPackages = [ agenix.packages.x86_64-linux.default ];
}
例如,在你的 flake.nix 文件中:
{
inputs.agenix.url = "github:ryantm/agenix";
# ...
outputs = { self, nixpkgs, agenix }: {
# change `yourhostname` to your actual hostname
nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
# ...
{
environment.systemPackages = [ agenix.packages.${system}.default ];
}
];
};
};
}
教程
-
你要部署密钥的目标系统必须已经存在,并且 在其上运行
sshd,以便它已在/etc/ssh/中生成 SSH 主机密钥。 -
创建一个目录来存储密钥以及用于列出密钥及其公钥的
secrets.nix文件:$ mkdir secrets $ cd secrets $ touch secrets.nix此
secrets.nix文件不会导入到你的 NixOS 配置中。 它仅用于agenixCLI 工具(如下例所示),以了解用于加密的公钥。 -
将公钥添加到你的
secrets.nix文件中:let user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH"; user2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILI6jSq53F/3hEmSs+oq9L4TwOo1PrDMAgcA1uo1CCV/"; users = [ user1 user2 ]; system1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPJDyIr/FSz1cJdcoW69R+NrWzwGK/+3gJpqD1t8L2zE"; system2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1"; systems = [ system1 system2 ]; in { "secret1.age".publicKeys = [ user1 system1 ]; "secret2.age".publicKeys = users ++ systems; "armored-secret.age" = { publicKeys = [ user1 ]; armor = true; }; }这些是稍后将能够使用其对应的私钥解密
.age文件的用户和系统。 也可以在此处提供 armor 选项,以确保文件以 Base64 PEM 文本格式输出,这对于更可读的 diff 很有用。 你可以从以下位置获取公钥- 你的本地计算机,通常在
~/.ssh中,例如~/.ssh/id_ed25519.pub。 - 从正在运行的目标机器通过
ssh-keyscan获取:$ ssh-keyscan <ip-address> ... ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1 ... - 从 GitHub,例如 https://github.com/ryantm.keys。
- 你的本地计算机,通常在
-
创建一个密钥文件:
$ agenix -e secret1.age它将在 $EDITOR 环境变量中配置的应用程序里打开一个临时文件。 当你保存该文件时,其内容将使用
secrets.nix文件中提到的所有公钥进行加密。 -
将密钥添加到 NixOS 模块配置中:
{ age.secrets.secret1.file = ../secrets/secret1.age; }当
age.secrets属性集包含一个密钥时,agenixNixOS 模块稍后会自动解密并将该密钥挂载到默认路径/run/agenix/secret1下。 在此处,secret1.age文件成为你的 NixOS 部署的一部分,即移入 Nix 存储中。 -
在您的配置中引用密钥的挂载路径:
{ users.users.user1 = { isNormalUser = true; hashedPasswordFile = config.age.secrets.secret1.path; }; }您可以在其他配置中引用(稍后)未加密密钥的挂载路径。 因此,
config.age.secrets.secret1.path默认将包含路径/run/agenix/secret1。 -
像往常一样使用
nixos-rebuild或 其他部署工具。secret1.age文件将像任何其他 Nix 包一样被复制到目标机器。 然后,它将按照前述说明进行解密并挂载。 -
编辑密钥文件:
$ agenix -e secret1.age它假设您的 SSH 私钥位于
~/.ssh/。 为了解密并打开.age文件进行编辑,您需要使用其中一个用于加密的公钥对应的私钥。您可以使用-i显式传递要使用的私钥,例如:$ agenix -e secret1.age -i ~/.ssh/id_ed25519
使用 agenix 与 home-manager
home-manager 模块遵循与 NixOS 模块相同的一般原则,但仅限于单个用户。以下是使用方法:
- 按照“安装”部分所示,将 home-manager 模块添加到您的配置中。
- 定义您的 SSH 身份和密钥:
{
age = {
identityPaths = [ "~/.ssh/id_ed25519" ];
secrets = {
example-secret = {
file = ../secrets/example-secret.age;
};
};
};
}
- 在你的主配置中引用你的密钥:
{
programs.some-program = {
enable = true;
hashedPasswordFile = config.age.secrets.example-secret.path;
};
}
当你运行 home-manager switch 时,你的密钥将被解密到一个用户特定的目录(在 Linux 上通常是 $XDG_RUNTIME_DIR/agenix,在 Darwin 上是一个临时目录),并可在你的配置中引用。
参考
age 模块参考
age.secrets
age.secrets 密钥的 attrset。您始终需要使用此
配置选项。默认值为 {}。
age.secrets.<name>.file
age.secrets.<name>.file 是此密钥的加密 .age 的路径。这是唯一必需的密钥选项。
示例:
{
age.secrets.monitrc.file = ../secrets/monitrc.age;
}
age.secrets.<name>.path
age.secrets.<name>.path 是密钥解密后的输出路径。
默认为 /run/agenix/<name> (config.age.secretsDir/<name>)。
定义不同路径的示例:
{
age.secrets.monitrc = {
file = ../secrets/monitrc.age;
path = "/etc/monitrc";
};
}
对于许多服务,您无需设置此项。相反,请在配置中引用
解密路径,使用
config.age.secrets.<name>.path。
引用路径的示例:
{
users.users.ryantm = {
isNormalUser = true;
hashedPasswordFile = config.age.secrets.passwordfile-ryantm.path;
};
}
builtins.readFile 反模式
{
# Do not do this!
config.password = builtins.readFile config.age.secrets.secret1.path;
}
这可能导致明文被放入世界可读的 Nix 存储中。相反,请让您的服务在运行时读取明文路径。
age.secrets.<name>.mode
age.secrets.<name>.mode 是解密后密钥的权限模式,
采用 chmod 可理解的格式。通常,您只需将其与
age.secrets.<name>.owner 和
age.secrets.<name>.group 配合使用。
示例:
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
age.secrets.<name>.owner
age.secrets.<name>.owner 是解密后文件所有者的用户名。通常,您只需将其与
age.secrets.<name>.mode 和 age.secrets.<name>.group 结合使用
示例:
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
age.secrets.<name>.group
age.secrets.<name>.group 是解密后文件的
组名。通常,您只需将其与
age.secrets.<name>.owner 和 age.secrets.<name>.mode 结合使用
示例:
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
age.secrets.<name>.symlink
age.secrets.<name>.symlink 是一个布尔值。如果为 true(默认值),
密钥将符号链接到 age.secrets.<name>.path。如果为 false,密钥
将被复制到 age.secrets.<name>.path。通常,你希望保持
此值为 true,因为它能安全地清理不再使用的密钥。
(符号链接仍然存在,但会失效。)如果
为 false,你需要负责在停止使用密钥后自行清理密钥。
某些程序不喜欢跟随符号链接(例如 Elasticsearch 等 Java 程序)。
示例:
{
age.secrets."elasticsearch.conf" = {
file = ../secrets/elasticsearch.conf.age;
symlink = false;
};
}
age.secrets.<name>.name
age.secrets.<name>.name 是文件解密后的名称字符串。默认值为 attrpath 中的 <name>,但如果您希望文件名与属性名部分不同,可以单独设置。
一个名称与其 attrpath 不同的 secret 示例:
{
age.secrets.monit = {
name = "monitrc";
file = ../secrets/monitrc.age;
};
}
age.ageBin
age.ageBin age 二进制文件的路径字符串。通常,您
无需更改此项。默认值为 age/bin/age。
覆盖 age.ageBin 的示例:
{pkgs, ...}:{
age.ageBin = "${pkgs.age}/bin/age";
}
age.identityPaths
age.identityPaths 是一个路径列表,用于尝试使用接收者密钥来
解密机密。默认情况下,它是位于
config.services.openssh.hostKeys 中的 rsa 和 ed25519 密钥,在 NixOS 上通常无需
更改此项。列表项应为字符串("/path/to/id_rsa"),而非
nix 路径(../path/to/id_rsa),因为后者会将您的私钥复制到
nix store 中,而这正是 agenix 旨在避免的情况。在运行时,
至少一个文件路径必须存在且能够解密
相关的机密。覆盖 age.identityPaths 的示例:
{
age.identityPaths = [ "/var/lib/persistent/ssh_host_ed25519_key" ];
}
age.secretsDir
age.secretsDir 是默认情况下密钥符号链接指向的目录。通常,您无需更改此设置。默认值为
/run/agenix。
覆盖 age.secretsDir 的示例:
{
age.secretsDir = "/run/keys";
}
age.secretsMountPoint
age.secretsMountPoint 是密钥代在创建并建立符号链接之前的目录。通常,您无需
更改此项。默认值为 /run/agenix.d。
覆盖 age.secretsMountPoint 的示例:
{
age.secretsMountPoint = "/run/secret-generations";
}
age-home 模块参考
home-manager 模块提供与 NixOS 模块类似的选项,但作用范围限定于单个用户。
age.secrets
age.secrets 密钥的 attrset。您始终需要使用此
配置选项。默认值为 {}。
age.secrets.<name>.file
age.secrets.<name>.file 是此密钥的加密 .age 的路径。这是唯一必需的密钥选项。
age.secrets.<name>.path
age.secrets.<name>.path 是密钥解密后的输出路径。
在 Linux 上默认为 $XDG_RUNTIME_DIR/agenix/<name>,
在 Darwin 上默认为 $(getconf DARWIN_USER_TEMP_DIR)/agenix/<name>。
age.secrets.<name>.mode
age.secrets.<name>.mode 是解密后密钥的权限模式,
采用 chmod 可识别的格式。
age.secrets.<name>.symlink
age.secrets.<name>.symlink 是一个布尔值。如果为 true(默认值),
secrets 会被符号链接到 age.secrets.<name>.path。如果为 false,secrets
会被复制到 age.secrets.<name>.path。
age.identityPaths
age.identityPaths 是一个用于解密的 SSH 私钥路径列表。
这是一个必需选项;没有默认值。
age.secretsDir
age.secretsDir 是默认情况下 secrets 被符号链接到的目录。在 Linux 上默认为 $XDG_RUNTIME_DIR/agenix,
在 Darwin 上默认为 $(getconf DARWIN_USER_TEMP_DIR)/agenix。
age.secretsMountPoint
age.secretsMountPoint 是在符号链接创建之前生成密钥的目录。在 Linux 上默认为 $XDG_RUNTIME_DIR/agenix.d,在 Darwin 上默认为 $(getconf DARWIN_USER_TEMP_DIR)/agenix.d。
agenix CLI 参考
agenix - edit and rekey age secret files
agenix -e FILE [-i PRIVATE_KEY]
agenix -r [-i PRIVATE_KEY]
options:
-h, --help show help
-e, --edit FILE edits FILE using $EDITOR
-r, --rekey re-encrypts all secrets with specified recipients
-d, --decrypt FILE decrypts FILE to STDOUT
-i, --identity identity to use when decrypting
-v, --verbose verbose output
FILE an age-encrypted file
PRIVATE_KEY a path to a private SSH key used to decrypt file
EDITOR environment variable of editor to use when editing FILE
If STDIN is not interactive, EDITOR will be set to "cp /dev/stdin"
RULES environment variable with path to Nix file specifying recipient public keys.
Defaults to './secrets.nix'
重新加密
如果您更改了 secrets.nix 中的公钥,您应该重新加密您的
密钥:
$ agenix --rekey
要对密钥进行重新加密,你必须能够解密它。由于 age 的加密算法中存在随机性,即使身份标识未变,重新加密后文件也总会发生变化。(未来可以通过从 age 文件中读取身份标识来改进这一点。)
覆盖 age 二进制文件
agenix CLI 默认使用 age 作为其 age 实现,你可以像这样在 Flakes 中使用 rage 实现:
{pkgs,agenix,...}:{
environment.systemPackages = [
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
];
}
社区与支持
支持及开发讨论可在 GitHub 上获取, 也可通过 Matrix。
威胁模型/警告
本项目尚未由安全专家进行审计。
不熟悉 age 的人可能会惊讶地发现密钥并未经过
身份验证。这意味着任何拥有密钥文件写入权限的攻击者
都可以修改密钥,因为公钥是暴露的。
乍一看这似乎不是问题,因为更改配置本身
就可能轻易暴露密钥。然而,审查配置更改比审查随机密钥(例如
4096 位 rsa 密钥)更容易。如果像其他实现如 GPG 或
sops 那样拥有消息
认证码(MAC),这个问题就可以解决,但在 age 中为了
简洁而省略了这一点。
此外,您只应加密那些在未来被解密时能够使其变得无用的密钥,并准备好定期轮换它们,因为 age 截至 2024 年 6 月 19 日并非后量子安全](https://github.com/FiloSottile/age/discussions/231#discussioncomment-3092773),因此如果威胁行为者能够访问您的加密密钥(例如通过其在公共仓库中的使用),他们可以利用 先收割,后解密 策略,现在存储您的密钥以便日后解密,包括发现重大漏洞从而暴露密钥的情况。详情请见 https://github.com/FiloSottile/age/issues/578。
贡献
- 主分支受保护,禁止直接推送
- 所有更改必须通过 GitHub PR 审查并获得至少一个批准
- PR 标题和提交信息应以以下类别中的至少一个作为前缀:
- contrib - 改善项目开发的内容
- doc - 文档
- feature - 新功能
- fix - 错误修复
- 请为新功能更新或编写集成测试
- 使用
nix fmt格式化 nix 代码
测试
你可以使用以下命令运行测试
nix flake check
你可以以交互模式运行集成测试,如下所示:
nix run .#checks.x86_64-linux.integration.driverInteractive
启动后,输入 run_tests() 以运行测试。
致谢
本项目基于 Mic92 创建的 sops-nix。感谢 Mic92 提供的灵感与建议。