🔐 agenix-rekey
这是agenix的一个扩展,它允许你摆脱维护secrets.nix
文件的麻烦,通过在需要的地方自动重新加密密钥。它还允许你为密钥定义多功能生成器,以便自动引导它们。这个扩展是一个仅支持flakes的项目,可以与常规的agenix一起使用。
要使用重新加密功能,你需要通过主密钥(YubiKey、FIDO2密钥、TPM或普通age身份)加密密钥并将其存储在你的仓库中,agenix-rekey将自动为任何需要这些密钥的主机重新加密。强烈推荐使用YubiKey/FIDO2密钥,它将为你提供流畅的重新加密体验。总之,你可以获得:
- 🔑 单一主密钥。 你仓库中的任何内容都由你的主YubiKey/FIDO2密钥或age身份加密。
- ➡️ 主机密钥推断。 无需手动跟踪哪个密钥用于哪个主机 - 不需要
secrets.nix
。 - ✔️ 减少密钥管理。 重新加密的密钥永远不需要添加到你的flake仓库中,因此你只需要跟踪实际的密钥。此外,即使你的仓库是公开的,泄露的主机密钥也不会让攻击者解密旧的已签入的密钥。
- 🦥 延迟重新加密。 只有在必要时才进行重新加密,因为结果是加密的,所以可以缓存在本地目录中。如果添加/更改了密钥或修改了主机密钥,你将自动收到重新加密的提示。
- 🚀 简化主机引导。 自动重新加密可以为未知目标主机使用虚拟公钥,这样你可以引导一个公钥尚未知的新系统。运行时解密当然会失败,但随后会生成ssh主机密钥。
- 🔐 密钥生成。 你可以定义生成器来引导密钥。如果你需要为服务生成随机密码、需要随机的wireguard私钥/预共享密钥,或者需要将几个密钥聚合成一个派生密钥(例如生成.htpasswd文件),这非常有用。
为了正常运行,agenix-rekey必须进行一些nix技巧。你可以在下面阅读更多关于它如何工作的信息。备注:
- 自
age-plugin-yubikey
0.4.0版本起,PIN只需要输入一次。使用密码保护的主密钥永远不会有这个好处,每次重新加密操作都需要输入密码。除非缓存密钥,否则无法避免这种情况,而我不想这样做。
概述
使用agenix-rekey时,你将拥有一个agenix
命令来在你的flake上运行与密钥相关的操作。这是对agenix提供的命令的替代,你不再需要原来的命令。有几个应用/子命令可以用来管理你的密钥:
agenix generate
:生成尚不存在且设置了生成器的任何密钥。agenix edit
:使用$EDITOR
创建/编辑密钥。可以加密现有文件。agenix rekey
:为需要的主机重新加密密钥。- 使用
agenix <命令> --help
获取具体使用信息。
一般工作流程非常简单,因为每当需要时,你都会自动收到运行agenix rekey
的提示(构建将失败并告诉你)。
安装
要使用agenix-rekey,你需要将agenix-rekey添加到你的flake.nix
中,在你的主机中导入提供的NixOS模块,并在你的flake中公开一些信息,以便agenix-rekey知道在哪里查找密钥。还提供了flake-parts模块(参见本节末尾的示例)。
要获得agenix
命令,你可以使用nix shell github:oddlama/agenix-rekey
进入一个临时可用的shell,或者将提供的包agenix-rekey.packages.${system}.default
添加到你的devshell中,如下所示。
如果你不想使用包装器,你也可以通过你的flake直接调用脚本,使用nix run .#agenix-rekey.<system>.<app>
,这在你自己的脚本中可能很有用。
{
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.agenix.url = "github:ryantm/agenix";
inputs.agenix-rekey.url = "github:oddlama/agenix-rekey";
# 确保覆盖nixpkgs版本以跟随你的flake,
# 否则派生路径可能不匹配(当使用storageMode = "derivation"时),
# 导致找不到重新加密的密钥!
inputs.agenix-rekey.inputs.nixpkgs.follows = "nixpkgs";
# ...
outputs = { self, nixpkgs, agenix, agenix-rekey }: {
# 示例系统配置
nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
agenix.nixosModules.default
agenix-rekey.nixosModules.default
];
};
# 在你的flake中公开必要的信息,以便agenix-rekey
# 知道在哪里查找密钥和路径。
#
# 确保这里传递的pkgs来自与你的主机在`nixosConfigurations`中使用的
# pkgs相同的nixpkgs版本,否则重新加密的派生将无法找到!
agenix-rekey = agenix-rekey.configure {
userFlake = self;
nodes = self.nixosConfigurations;
# colmena示例:
# inherit ((colmena.lib.makeHive self.colmena).introspect (x: x)) nodes;
};
}
# 可选:仅当你想在devshell中使用agenix命令时才需要这部分。
// flake-utils.lib.eachDefaultSystem (system: rec {
pkgs = import nixpkgs {
inherit system;
overlays = [ agenix-rekey.overlays.default ];
};
devShells.default = pkgs.mkShell {
packages = [ pkgs.agenix-rekey ];
# ...
};
});
}
与flake-parts一起使用
{
inputs.flake-parts.url = "github:hercules-ci/flake-parts";
inputs.agenix.url = "github:ryantm/agenix";
inputs.agenix-rekey.url = "github:oddlama/agenix-rekey";
# 确保覆盖nixpkgs版本以跟随你的flake,
# 否则派生路径可能不匹配(当使用storageMode = "derivation"时),
# 导致找不到重新加密的密钥!
inputs.agenix-rekey.inputs.nixpkgs.follows = "nixpkgs";
# ...
outputs = inputs:
inputs.flake-parts.lib.mkFlake {inherit inputs;} {
imports = [
inputs.agenix-rekey.flakeModule
];
perSystem = {config, pkgs, ...}: {
# 将`config.agenix-rekey.package`添加到你的devshell中,
# 以便轻松访问`agenix`命令包装器。
devShells.default = pkgs.mkShell {
nativeBuildInputs = [ config.agenix-rekey.package ];
};
# 如果你想更改考虑重新加密的主机,可以定义agenix-rekey.nodes。
# 参考agenix-rekey的flake.parts部分以查看所有可用选项。
agenix-rekey.nodes = inputs.self.nixosConfigurations; # (技术上不需要,因为它已经是默认值)
};
};
}
你可以在两种存储模式之间选择用于重新加密的密钥,它们在根本上是不同的。你可以自由地在它们之间切换,更多信息请参见这里。
使用
由于agenix-rekey只是agenix的一个扩展,你所知道的关于agenix的一切仍然照常适用。除了指定关于你的主密钥的元信息外,要使用重新加密功能,你唯一需要改变的是在你的密钥上指定rekeyFile
而不是file
。完整的设置过程如下:
-
对于每个主机,你必须提供一个用于重新加密的公钥,并选择用于解密存储在你仓库中的密钥的主身份。
hostPubkey
显然对每个主机都不同,但所有其他选项(如你的主身份)通常在主机之间是相同的。你可以在下面的API参考中找到更多选项。默认情况下,我们将使用本地存储模式,该模式将在你自己的仓库中存储重新加密的密钥。
{
age.rekey = {
# 使用 `ssh-keyscan` 或查看 ~/.ssh/known_hosts 获取此值
hostPubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...";
# 用于解密的主身份路径。详见选项说明。
masterIdentities = [ ./your-yubikey-identity.pub ];
#masterIdentities = [ "/home/myuser/master-key" ]; # 外部主密钥
#masterIdentities = [
# # 可以使用以下替代语法指定身份,
# # 这可以避免加密时不必要的提示。
# {
# identity = "/home/myuser/master-key.age"; # 密码保护的外部主密钥
# pubkey = "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3290gq"; # 显式指定公钥
# }
#];
storageMode = "local";
# 选择一个目录来存储此主机的重新加密秘密。
# 不能与其他主机共享。请从 flake 的根目录引用此路径,
# 而不是使用直接路径字面量,如 ./secrets
localStorageDir = ./. + "/secrets/rekeyed/${config.networking.hostName}";
};
}
-
使用 (r)age 和主密钥加密一些秘密。
agenix-rekey
提供了一个名为agenix
的命令行工具, 它允许您使用喜欢的$EDITOR
轻松创建/编辑秘密, 并根据步骤 1 中的设置自动使用正确的身份进行解密和加密。理想情况下,您应该已经按照安装部分的说明将其添加到开发环境中, 否则您可以通过
nix run github:oddlama/agenix-rekey -- <子命令> [选项]
临时运行该工具。# 创建新的或编辑现有的秘密 agenix edit secret1.age # 或加密现有文件 agenix edit -i plain.txt secret1.age # 如果没有提供参数,将显示一个包含所有定义的秘密的交互式列表 # 以便您选择要创建/编辑的秘密 agenix edit # 或者,您当然可以使用 (r)age 手动加密内容 echo "secret" | rage -e -i ./your-yubikey-identity.pub > secret1.age
在选择
$EDITOR
时要小心,通过撤销历史或缓存等方式,它可能会泄露秘密信息。 对于vim
和nvim
,此应用程序会自动禁用相关选项以确保安全使用。 -
在配置中定义秘密并使用它。这类似于经典的 agenix,但现在使用
rekeyFile
代替file
(它会为file
生成定义)。{ age.secrets.secret1.rekeyFile = ./secret1.age; services.someService.passwordFile = config.age.secrets.secret1.path; }
-
像往常一样使用
nixos-rebuild
或您喜欢的部署工具部署系统。 如果需要重新加密,您会在构建失败时收到提示。 由于我们刚刚完成初始设置,您应该立即重新加密:> agenix rekey -a # 使用本地存储模式时,-a 会将它们添加到 git
别忘了afterwards添加重新加密的秘密,使它们对构建过程可见。
[!警告] 如果使用
storageMode = "derivation"
,agenix rekey
必须能够设置额外的 沙盒路径。为此,您需要将age.rekey.cacheDir
添加为全局额外沙盒路径 (不要将用户添加到 trusted-users 中,这基本上会授予他们 root 访问权限!):nix.settings.extra-sandbox-paths = ["/tmp/agenix-rekey.${config.users.users.youruser.uid}"];
有关用户无关设置的更多信息,请参见 issue #9。
[!注意] 如果使用
storageMode = "derivation"
,并且将配置部署到 远程系统,需要确保包含重新加密秘密的正确 derivation 从本地存储 复制到远程主机的存储中。任何在本地构建并使用
nix copy
(或等效工具)将 derivation 复制到 远程系统的工具都会自动工作,因此无需额外注意。 只有当您严格在远程系统上构建时,可能需要手动复制这些秘密。 您可以使用agenix rekey --show-out-paths
或直接引用nixosConfigurations.<host>.config.age.rekey.derivation
来定位它们。
使用 FIDO2 密钥代替 YubiKey
这个 agenix 扩展也可以使用 FIDO2 密钥而不是 YubiKey,但您需要 稍微调整设置(感谢 @Arbel-arad 指出这一点):
- 首先,您需要一个支持
hmac-secret
扩展的 FIDO2 密钥,可以通过运行fido2-token -I
检查 - 通过设置
age.rekey.agePlugins = [pkgs.age-plugin-fido2-hmac];
添加必要的插件 - 运行
age-plugin-fido2-hmac -g
在 FIDO2 密钥上生成凭证 - 如果询问是否要单独的身份文件,选择是。它会打印接收者地址和密钥握柄。
- 指定密钥握柄身份文件并向 agenix-rekey 提供公钥:
age.rekey.masterIdentities = [{ identity = ./mykey.hmac; pubkey = "age123456..."; }];
秘密生成
使用 agenix-rekey,您可以为秘密定义生成器,用于引导秘密或从其他秘密派生秘密。
在最简单的情况下,您可以引用预定义的现有生成器,
下面的示例将使用 age.generators.passphrase
生成器生成一个随机的 6 个单词的口令:
{
age.secrets.randomPassword = {
rekeyFile = ./secrets/randomPassword.age;
generator.script = "passphrase";
};
}
预定义生成器
alnum
- 生成一个长(48 个字符)的字母数字密码。base64
- 生成一个长(32 个字符)的 base64 编码密码。hex
- 生成一个长(24 个字符)的十六进制编码密码。passphrase
- 生成一个由六个单词组成的、以空格分隔的口令。dhparams
- 生成可用于完美前向保密的 Diffie–Hellman 参数。 详见 Diffie-Hellman_parameters。ssh-ed25519
- 使用当前主机名生成 ED-25519 SSH 密钥对。
自定义生成器
您还可以定义自己的生成器,方法是在 age.generators
中创建一个条目
以制作可重用的生成器(如上面的 "passphrase"
),或直接将
age.secrets.<name>.generator
设置为生成器定义。
生成器是一个包含两个属性的集合,一个 script
和一个可选的 dependencies
。
script
必须是一个字符串,引用全局定义的生成器之一,
或者是一个函数。这个函数接收一个带参数的属性集,并必须返回一个 bash
脚本,该脚本实际生成所需的秘密并将其写入标准输出。
一个非常简单(且糟糕)的生成器可能是 { ... }: "echo very-secret"
。
传递给 script
的参数将包含一些有用的属性,我们可以用它们来定义生成脚本。
参数 | 描述 |
---|---|
name | 要生成的秘密的名称,在 age.secrets.<name> 中定义 |
secret | 要生成的秘密的定义 |
lib | 方便访问 nixpkgs 库 |
pkgs | 运行生成脚本的主机的包集。在脚本中不要使用任何其他包集! |
file | 此函数返回后将写入的 .age 文件的实际路径,内容会被加密。用于将额外信息写入相邻文件。 |
deps | 我们 dependencies 中的所有秘密文件列表。每个条目是一个包含 { name, host, file } 的集合,对应于秘密 nixosConfigurations.${host}.age.secrets.${name} 。file 是秘密的 rekeyFile 的真实源位置。您可以使用 ${decrypt} ${escapeShellArg dep.file} 提取明文。 |
decrypt | 基本 rage 命令,可以使用定义的 masterIdentities 将秘密解密到标准输出。 |
... | 用于未来/未使用的参数 |
首先,让我们看一个定义非常简单的生成器的例子,它创建更长的口令。
注意我们使用传递的 pkgs
集合而不是配置中的包集合。
{
# 允许您使用 "long-passphrase" 作为生成器。
age.generators.long-passphrase = {pkgs, ...}: "${pkgs.xkcdpass}/bin/xkcdpass --numwords=10";
}
另一个常见情况是生成密钥对,我们还希望直接
派生匹配的公钥并将其存储在相邻的 .pub
文件中:
{
age.generators.wireguard-priv = {pkgs, file, ...}: ''
priv=$(${pkgs.wireguard-tools}/bin/wg genkey)
${pkgs.wireguard-tools}/bin/wg pubkey <<< "$priv" > ${lib.escapeShellArg (lib.removeSuffix ".age" file + ".pub")}
echo "$priv"
'';
}
通过使用deps
和decrypt
,我们还可以生成依赖于其他密钥值的密钥。
当你想从几个自动生成的明文密码生成一个.htpasswd
文件时,你可能会遇到这种情况:
{
# 生成一个随机密码
age.secrets.basic-auth-pw = {
rekeyFile = ./secrets/basic-auth-pw.age;
generator.script = "alnum";
};
# 从几个随机密码生成htpasswd
age.secrets.some-htpasswd = {
rekeyFile = ./secrets/htpasswd.age;
generator = {
# 所有这些密钥将首先生成,并在生成此密钥时将它们的路径作为`deps`传递给`script`。
# 你可以引用其他系统的age密钥,只要所有相关系统都通过nixosConfigurations参数传递给agenix-rekey应用定义。
dependencies = [
# 本地密钥
config.age.secrets.basic-auth-pw
# 其他机器的密钥
nixosConfigurations.machine2.config.age.secrets.basic-auth-pw
nixosConfigurations.machine3.config.age.secrets.basic-auth-pw
];
script = { pkgs, lib, decrypt, deps, ... }:
# 对于每个依赖项,我们可以使用`decrypt`获取明文。
# 我们通过apache的htpasswd运行它来创建htpasswd条目。
# 由于所有命令都输出到stdout,我们自动得到一个有效的htpasswd文件。
lib.flip lib.concatMapStrings deps ({ name, host, file }: ''
echo "Aggregating "''${lib.escapeShellArg host}:''${lib.escapeShellArg name} >&2
# 解密包含明文密码的依赖项,
# 并通过htpasswd运行它以生成bcrypt哈希
${decrypt} ${lib.escapeShellArg file} \
| ${pkgs.apacheHttpd}/bin/htpasswd -niBC 10 ${lib.escapeShellArg host}
'');
};
};
}
使用age代替rage
如果由于某些原因你不想使用rage,你可以在顶层配置调用中指定一个兼容的替代工具(或者如果你使用flake-parts,可以通过选项指定):
agenix-rekey = agenix-rekey.configure {
# ...
agePackage = p: p.age;
};
存储模式
你可以在两种存储模式之间选择重新加密的密钥,这两种模式本质上是不同的。你可以随时自由地在它们之间切换。
选项一是将重新加密的密钥存储在你的仓库本地(local
),选项二是将它们透明地存储在自动创建的派生中(derivation
)。
如果不确定,请使用更灵活和纯粹的local
,但请记住,derivation
在某些情况下可能更安全。它使用更多的"魔法"来隐藏一些细节,如果你只在一台主机上构建并且不关心远程构建/CI,它可能更简单易用。
选择取决于你的组织偏好和威胁模型。
derivation
以前这是默认模式。每个主机的所有重新加密的密钥将被收集到一个派生中,当使用agenix rekey
构建时,该派生将它们复制到nix存储中。
- 优点: 整个过程是无状态的,重新加密的密钥永远不会提交到你的仓库。
- 缺点: 除非手动上传重新加密后的派生到CI,否则你无法轻松地从没有访问你的(yubi)密钥的CI/任何主机构建你的主机。
local
运行agenix rekey
时,所有重新加密的密钥将保存到你的flake中的本地文件夹。Agenix将直接使用这些本地文件,而不需要任何额外的派生。这是更简单的方法,并且边缘情况更少。
- 优点: 系统构建保持纯粹,不需要沙盒技巧。 -> 可以在没有访问(yubi)密钥的情况下构建系统。
- 缺点: 如果你的仓库是公开的,并且你的一台主机被攻破,攻击者可能会解密为该主机加密的任何密钥。这包括git历史中的密钥。
它是如何工作的?
核心问题是在构建系统时动态重新加密密钥从根本上是不可能的,因为这是一个不纯的操作。它总是需要以主密码的形式输入外部输入,或者必须与YubiKey通信。
第二个问题是构建你的系统需要重新加密的密钥在nix存储中可用,我们希望在不要求你在git中跟踪它们的情况下实现这一点。
处理不纯性
agenix-rekey
通过采用两步方法解决了不纯性问题。通过添加agenix-rekey,你隐式地通过你的flake定义了一个可以在你的主机环境中运行的脚本,因此能够提示输入密码或读取YubiKeys。
它可以运行age
来重新加密密钥并将它们存储在临时缓存目录中。
预测存储路径以避免跟踪重新加密的密钥
更复杂的第二个问题是通过为每个主机使用一个特殊的派生来预测重新加密密钥的存储路径来解决的。 当构建过程以传递方式调用时,这个派生总是被设置为失败,这总是意味着需要重新加密。
agenix rekey
命令将构建相同的派生,但可以特殊访问重新加密的密钥,这些密钥将暂时存储在/tmp
的可预测路径中,沙盒允许访问/tmp
,从而解决了不纯性问题。之后运行构建将成功,因为派生现在已经构建并在你的本地存储中可用。
❄️ 模块选项
age.secrets
这些是agenix公开的密钥选项。有关所有基本属性的描述,请参见age.secrets
。
以下是agenix-rekey添加的其他选项的文档。
age.secrets.<name>.rekeyFile
类型 | nullOr path |
---|---|
默认值 | null |
示例 | ./secrets/password.age |
此密钥的加密.age文件的路径。该文件必须使用给定的age.rekey.masterIdentities
之一加密,而不是使用特定于主机的密钥加密。
此密钥将自动为使用它的主机重新加密,并且生成的特定于主机的.age文件将设置为实际的file
属性。因此,这自然与直接指定file
互斥。
如果你想避免使用secrets.nix
文件,只使用重新加密的密钥,你应该始终使用此选项而不是file
。
age.secrets.<name>.generator
类型 | nullOr (either str generatorType) |
---|---|
默认值 | null |
示例 | { script = "passphrase"; } |
如果定义,当密钥不存在时,将使用此生成器来引导此密钥。
age.secrets.<name>.generator.dependencies
类型 | listOf unspecified |
---|---|
默认值 | [] |
示例 | [ config.age.secrets.basicAuthPw1 nixosConfigurations.machine2.config.age.secrets.basicAuthPw ] |
此密钥依赖的其他密钥。这保证在最终的agenix generate
脚本中,所有依赖项将在生成此密钥之前生成,允许你通过传递的decrypt
函数使用它们的输出。
给定的依赖项将通过deps
参数传递给定义的script
,这将是一个包含它们真实源位置(rekeyFile
)的列表,没有特定顺序。
这应该只引用config.age.secrets
中具有生成器的密钥定义。如果你想创建派生密钥,例如从几个基本认证密码生成.htpasswd文件,这很有用。
你可以引用其他系统的age密钥,只要所有相关系统都通过nixosConfigurations参数传递给agenix-rekey应用定义。
age.secrets.<name>.generator.script
类型 | either str (functionTo str) |
---|---|
示例 | 参见源代码或密钥生成。 |
这必须是全局定义的生成器的名称,或者是评估为脚本的函数。结果脚本将按原样添加到内部全局生成脚本中,并在任何沙盒之外运行。参考age.generators
以了解使用示例。
这允许你在必要时创建/覆盖相邻文件,例如当你还想为生成的私钥存储公钥时。 参考示例以了解参数的描述。生成的密钥应该写入stdout,任何信息或错误写入stderr。
请注意,脚本以set -euo pipefail
条件运行,作为运行agenix generate
的普通用户。
age.secrets.<name>.generator.tags
类型 | listOf str |
---|---|
默认值 | [] |
示例 | ["wireguard"] |
可用于引用使用此生成器的密钥的可选标签列表。
用于使用agenix generate -f -t wireguard
重新生成匹配特定标签的所有密钥。
age.generators
类型 | attrsOf (functionTo str) |
---|---|
默认值 | 定义了一些常见的密码生成器。详见源代码。 |
示例 | 参见源代码或密钥生成。 |
允许定义可重用的密钥生成器脚本。默认提供以下生成器:
alnum
:生成一个48字符长的字母数字字符串base64
:生成一个32字节随机数的base64字符串(长度44)hex
:生成一个24字节随机数的十六进制字符串(长度48)passphrase
:生成一个由6个单词组成的密码短语,用空格分隔dhparams
:生成4096位的dhparamsssh-ed25519
:生成一个ssh-ed25519私钥
age.rekey.generatedSecretsDir
类型 | nullOr path |
---|---|
默认值 | null |
示例 | ./secrets/generated |
存储所有生成的密钥的默认路径。
如果设置,对于任何定义了生成器的密钥,这将自动将age.secrets.<name>.rekeyFile
设置为此目录中的默认值。
age.rekey.storageMode
类型 | enum ["derivation" "local"] |
---|---|
默认值 | "local" |
示例 | "derivation" |
你可以选择两种存储模式来保存重新加密的密钥,这两种模式在本质上是不同的。你可以随时在它们之间自由切换。
第一种选择是将重新加密的密钥本地存储在你的仓库中(local
),第二种选择是将它们透明地存储在自动创建的派生中(derivation
)。
如果不确定,请使用更灵活和纯粹的local
模式,但请记住,derivation
模式在某些情况下可能更安全。它使用更多的"魔法"来隐藏一些细节,如果你只在一台主机上构建并且不关心远程构建/CI,它可能更简单易用。
选择取决于你的组织偏好和威胁模型。
derivation
之前这是默认模式。每个主机的所有重新加密的密钥将被收集到一个派生中,当使用agenix rekey
构建时,它们会被复制到nix存储中。
- 优点: 整个过程是无状态的,重新加密的密钥永远不会提交到你的仓库中。
- 缺点: 除非手动上传重新加密后的派生到CI,否则你无法轻松地从没有访问你的(Yubi)密钥的CI/任何主机上构建你的主机。
local
运行agenix rekey
时,所有重新加密的密钥将保存到你的flake中的本地文件夹。
Agenix将直接使用这些本地文件,无需任何额外的派生。这是一种更简单的方法,边缘情况更少。
- 优点: 系统构建保持纯粹,无需沙箱技巧。-> 可以在没有访问(Yubi)密钥的情况下构建系统。
- 缺点: 如果你的仓库是公开的,并且你的一个主机被入侵,攻击者可能解密曾经为该主机加密的任何密钥。这包括git历史中的密钥。
age.rekey.localStorageDir
类型 | path |
---|---|
示例 | ./. /* <- flake根目录 */ + "/secrets/rekeyed/myhost" /* 每个主机的单独文件夹 */ |
仅在storageMode = "local"
时使用。
重新加密密钥的本地存储目录。必须是你的仓库内的路径, 并且必须通过连接到你的flake的根目录来构造。请参照示例。
age.rekey.derivation
类型 | package |
---|---|
默认值 | 包含此主机重新加密密钥的派生 |
只读 | 是 |
仅在storageMode = "derivation"
时使用。
包含此主机重新加密密钥的派生。
这是为了在必要时可以将密钥上传到远程主机。
不能直接构建,请使用agenix rekey
代替。
age.rekey.cacheDir
类型 | str |
---|---|
默认值 | "/tmp/agenix-rekey.\"$UID\"" |
示例 | "\"\${XDG_CACHE_HOME:=$HOME/.cache}/agenix-rekey\"" |
仅在storageMode = "derivation"
时使用。
这是存储重新加密密钥的目录,以便派生构建器稍后可以找到它们。
必须是一个bash表达式,展开后得到要用作缓存的目录。默认情况下,缓存保存在/tmp中, 但你可以更改它(见示例)以在重启后保留缓存。 确保使用正确的引号,这必须是一个bash表达式,结果是一个单一的字符串。
实际的密钥将基于其输入内容哈希(从主机公钥和文件内容哈希派生)存储在目录中,
并存储为${cacheDir}/secrets/<ident-sha256>-<filename>
。这允许我们
在再次重新加密时重用已存在的重新加密密钥,同时为每个密钥提供一个确定性的路径。
age.rekey.forceRekeyOnSystem
类型 | nullOr str |
---|---|
默认值 | null |
示例 | "x86_64-linux" |
仅在storageMode = "derivation"
时使用。
如果设置,这将强制所有密钥在给定架构的系统上重新加密。 如果你有多个不同架构的主机,这很重要,因为你通常不希望在随机的远程主机上构建包含重新加密密钥的派生。
问题在于每个派生都至少依赖于一个特定的架构(通常是bash),因为它需要一个构建器来创建它。通常,构建器会使用构建包的架构,这是有道理的。由于它是派生输入的一部分,我们必须提前知道它以预测输出的位置。如果你有多个架构,那么我们就会有多个重新加密密钥的候选派生,但我们希望有一个可预测的单一派生。
如果你试图部署一个aarch64-linux系统,但你在x86_64-linux上没有二进制仿真,那么nix将不得不使用远程构建器来构建重新加密的密钥(因为派生此时需要aarch64-linux bash)。这个选项将覆盖传递给派生的pkgs集,使其总是使用指定架构的构建器。这样你就可以强制它总是需要x86_64-linux bash,从而允许你的本地系统构建它。
"自动"和好的方法是将其设置为builtins.currentSystem,但这也是不纯的,所以不幸的是你必须硬编码这个选项。
age.rekey.hostPubkey
类型 | coercedTo path (x: if isPath x then readFile x else x) str |
---|---|
默认值 | "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3290gq" |
示例 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI....." |
示例 | ./host1-pubkey.pub |
示例 | "/etc/ssh/ssh_host_ed25519_key.pub" |
重新加密时用作接收者的age公钥。这必须是age公钥文件的路径,或者是字符串形式的公钥本身。
如果你只管理一个主机,你可以在这里使用"/etc/ssh/ssh_host_ed25519_key.pub"
,
允许重新加密应用程序直接从你的系统读取你的公钥。
如果你管理多个主机,建议将每个主机的公钥副本存储在你的flake中,并在这里引用它们./secrets/host1-pubkey.pub
,
或者通过指定"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."
直接设置主机的公钥。
确保在这里永远不要使用私钥,因为它会最终出现在公共的nix存储中!
age.rekey.masterIdentities
类型 | listOf (coercedTo (coercedTo path toString str) <...> (submodule { identity = <...>; pubkey = <...> })) (完整签名) |
---|---|
默认值 | [] |
示例 | [./secrets/my-public-yubikey-identity.txt] |
示例 | [{identity = ./password-encrypted-identity.pub; pubkey = "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3290gq";}] |
在解密存储的密钥以重新加密给你的主机时,将呈现给rage
的age身份列表。如果给出多个身份,它们将按顺序尝试。
推荐的选项有:
- 使用以
.pub
结尾的拆分身份,其中不包含私有部分(YubiKey身份) - 使用nix存储外你的密钥的绝对路径("/home/myuser/age-master-key")
- 或者加密你的age身份并使用
.age
扩展名。你可以使用rage -p -o privkey.age privkey
来加密age身份,以保护它在你的存储中的安全。
如果你使用YubiKeys,你可以在这里指定多个拆分身份,并互换使用它们。 你将有选择跳过当前不可用的任何YubiKeys的选项。 为了防止加密过程中主密钥有时可能不可用的问题,可以使用另一种语法:
age.rekey.masterIdentities = [
{
# 这与指定身份的其他方式具有相同的类型。
identity = ./password-encrypted-identity.pub;
# 可选;这与 `age.rekey.hostPubkey` 具有相同的类型
# 并允许显式地将公钥与身份关联。
pubkey = "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3290gq";
}
];
如果显式指定了公钥,它将在加密过程中代替相关身份使用。这可以避免在使用密码加密的密钥文件时出现额外的提示,或在多用户场景中仅某些人可以访问的身份出现提示。对于Yubikey身份,如果身份文件中存在形如 Recipient: age1yubikey1<key>
的注释,可以自动从身份文件中提取公钥。这应该适用于由 age-plugin-yubikey
CLI生成的身份文件。有关自动提取公钥的确切标准的更多信息,请参阅拉取请求#28的描述。
对于主要身份可能根据情况变化的设置,例如在多用户设置中,每个人只能访问自己的个人Yubikey,请查看 AGENIX_REKEY_PRIMARY_IDENTITY
环境变量。
在这里使用路径时要小心,因为它们会被复制到nix存储中。使用分离身份是可以的,但如果您使用普通的age身份,请确保它们受密码保护。
age.rekey.extraEncryptionPubkeys
类型 | listOf (coercedTo path toString str) |
---|---|
默认值 | [] |
示例 | [./backup-key.pub "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3290gq"] |
示例 | ["age1yubikey1qwf..."] |
在使用 agenix edit FILE
时,默认情况下文件将为 age.rekey.masterIdentities
中的所有身份加密。在这里,您可以指定一组额外的公钥,所有机密也应该为这些公钥加密。这在您想要有一个备份身份能够解密所有机密但不应在尝试常规解密时使用的情况下很有用。
如果强制转换后的字符串是绝对路径,它将被视为接收者文件使用。否则,该字符串将被解释为公钥。
age.rekey.agePlugins
类型 | listOf package |
---|---|
默认值 | [rekeyHostPkgs.age-plugin-yubikey] |
示例 | [] |
在重新加密时应该可用于rage的插件列表。它们将在调用rage之前以最低优先级添加到PATH中,这意味着如果您在系统上安装了插件,那个插件将被优先使用,以避免破坏复杂的设置(例如WSL传递)。
⌨ 环境变量
AGENIX_REKEY_PRIMARY_IDENTITY
如果将此环境变量设置为公钥,agenix-rekey将尝试在显式指定或隐式提取的公钥中查找它(参见 age.rekey.masterIdentities
)。如果找到匹配的公钥,其关联的身份文件将在解密过程中被添加到传递给(r)age的所有其他身份参数之前。因此,它将首先尝试解密文件。这消除了在已知只有特定身份可用时手动跳过主身份的需要。它还允许为除第一个主身份列表中的Yubikey之外的Yubikey进行PIN缓存(参见此问题评论)。拉取请求#28的描述提供了更多详细信息。