使用 agenix 实现声明式密钥管理:基于 SSH 密钥与 age 加密的 GitOps 实践
1. 项目概述为什么我们需要 agenix在运维和开发工作中密钥管理一直是个让人头疼的“脏活累活”。想象一下这个场景你的项目需要连接数据库、调用第三方API、或者部署到云服务器这些操作都离不开各种密钥——数据库密码、API Token、SSH私钥。按照安全最佳实践这些敏感信息绝对不能明文写在代码仓库里。于是你可能会把它们放在一个单独的、被.gitignore忽略的.env文件中然后小心翼翼地通过聊天软件或者U盘分发给每个团队成员。更高级一点的做法可能会用到像HashiCorp Vault这样的专业密钥管理工具但这又引入了额外的复杂性和运维成本。有没有一种方法能让我们像管理普通代码一样用Git来版本化地管理这些密钥文件同时又保证它们的安全性并且整个流程足够简单、声明式能与现代基础设施即代码IaC的理念无缝融合这正是agenix要解决的问题。它不是一个全新的加密工具而是一个精巧的“粘合剂”和“流程优化器”将你已经拥有的SSH密钥和备受赞誉的age加密算法结合起来创造出一套既安全又优雅的密钥管理方案。简单来说agenix让你可以用Git安全地存储加密后的密钥文件在需要时用你本地的SSH私钥一键解密整个过程对NixOS这类声明式系统尤其友好。如果你厌倦了手动分发密钥和复杂的密钥管理服务器那么理解agenix的原理很可能为你打开一扇新的大门。2. 核心原理拆解SSH密钥与age算法的协同要理解agenix必须拆解其两大基石SSH密钥体系和age加密算法。它们的结合并非偶然而是为了解决GPG等传统工具在易用性和集成度上的痛点。2.1 SSH密钥现成的身份认证基础设施我们几乎每个人都用过SSH密钥对来免密登录服务器。它由一把私钥通常是~/.ssh/id_ed25519或~/.ssh/id_rsa和一把公钥同名文件加.pub后缀组成。公钥可以随意分发放到服务器的~/.ssh/authorized_keys文件中私钥则必须严格保密留在本地。agenix巧妙地利用了这套广泛部署且备受信任的体系。它不要求你生成和管理另一套独立的加密密钥对而是直接使用你现有的SSH公钥作为加密公钥用对应的SSH私钥进行解密。这样做有几个巨大优势零成本迁移你不需要为agenix单独生成和保管新的密钥直接复用现有SSH密钥降低了使用门槛和密钥管理负担。无缝集成现有流程团队成员的SSH公钥通常已经集中管理例如放在GitLab/GitHub上或内部的密钥仓库中。agenix可以直接使用这些公钥列表简化了授权管理。硬件安全模块HSM友好如果你的SSH私钥存储在YubiKey等硬件安全密钥中agenix解密时也能利用其硬件隔离的安全特性。在agenix的语境下一个密钥文件例如存储数据库密码的文件会被一个或多个SSH公钥加密。这意味着只有持有对应私钥的人或机器才能解密它。2.2 age算法现代、简单的加密工具age发音同“age”是“Actually Good Encryption”的缩写是一个由Filippo ValsordaGo密码学库维护者设计的现代加密工具和格式。它被设计用来替代GPG进行文件加密核心目标是简单和安全。为什么agenix选择age而不是GPG极简的API和概念模型age只有两个核心概念收件人用公钥加密和身份用私钥解密。没有复杂的信任网络、密钥环或过期日期。对于自动化脚本和工具集成来说这种简单性是至关重要的。现代密码学原语age默认使用X25519进行密钥交换ChaCha20-Poly1305进行对称加密。这些都是经过充分验证的现代算法避免了GPG中一些历史遗留算法的潜在弱点。原生支持SSH密钥age本身就可以将SSH-Ed25519和SSH-RSA公钥作为有效的加密公钥使用。这使得age与SSH生态系统的集成是天衣无缝的为agenix提供了直接的技术支撑。无元数据泄漏age加密文件格式非常简洁不会像某些格式那样泄漏收件人数量等元数据。注意虽然age支持多种密钥类型但agenix主要利用其SSH密钥支持能力。age也可以生成自己的原生密钥对age-keygen但在agenix的典型工作流中这并不是必须的。2.3 agenix 的工作流程声明式加密与解密理解了基础组件我们来看agenix如何将它们串联成一个声明式的工作流。整个过程围绕一个核心配置文件secrets.nix或其他你指定的名称。1. 定义秘密与授权secrets.nix这个Nix文件是“声明式”的体现。你在这里声明有哪些秘密文件如db-password.age。每个秘密文件应该由哪些SSH公钥加密即谁有权解密。# secrets.nix 示例 let # 从文件或变量中读取团队成员的SSH公钥 alicePubkey ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... aliceexample; bobPubkey ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... bobexample; serverPubkey ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... host; in { # 秘密文件路径 - 授权公钥列表 db-password.age.publicKeys [ alicePubkey bobPubkey serverPubkey ]; api-token.age.publicKeys [ alicePubkey ]; deploy-key.age.publicKeys [ serverPubkey ]; }这个文件本身不包含任何秘密可以安全地提交到Git仓库。它定义了一套访问控制策略。2. 编辑与加密秘密当你需要创建或更新一个秘密时使用agenix -e命令# 这会用$EDITOR打开一个临时文件你输入明文内容保存退出后 # agenix 会根据 secrets.nix 的配置用指定的公钥加密内容生成 .age 文件。 agenix -e db-password.age此时db-password.age文件的内容已经是密文可以被推送到Git仓库。3. 解密与使用秘密在需要用到秘密的环境如你的开发机或部署服务器上运行# 此命令会尝试用你本地的SSH私钥默认路径解密 db-password.age # 解密后的明文内容输出到标准输出。通常我们会重定向到文件或直接传递给程序。 agenix -d db-password.age解密的关键在于你的本地SSH私钥必须与secrets.nix中定义的某个公钥匹配。agenix会遍历secrets.nix中为该文件列出的所有公钥尝试用你的私钥解密。只要你是被授权的成员之一解密就会成功。4. 在NixOS中集成声明式部署的精华对于NixOS用户agenix的价值最大化。你可以在configuration.nix中直接引用这些秘密文件NixOS在构建系统时会自动调用agenix解密并将明文秘密放置在系统指定的位置如/run/secrets/db-password并设置严格的权限如仅root可读。# configuration.nix 片段 { config, pkgs, ... }: { age.secrets.db-password { file ./secrets/db-password.age; # 指向加密文件 path /run/secrets/db-password; # 解密后放置的位置 owner postgres; # 设置文件所有者 mode 0400; # 设置文件权限仅所有者可读 }; services.postgresql { enable true; # 直接引用解密后的秘密文件路径 initialScript config.age.secrets.db-password.path; }; }这样秘密的管理完全声明式化了。更新秘密只需重新编辑加密文件并部署配置无需手动登录每台服务器。3. 实战部署从零搭建 agenix 管理流程理论讲完了我们动手搭建一套完整的agenix工作流。假设我们有一个小团队需要管理一个Web应用的后端数据库密码和API密钥。3.1 环境准备与工具安装首先确保你的系统有age和agenix工具。agenix本身是一个Nix Flake应用但也可以通过其他包管理器安装。# 对于Nix/NixOS用户推荐 nix profile install github:ryantm/agenix # 对于其他Linux/macOS用户可以从源码构建或查找第三方包 # 例如使用Homebrew (macOS) brew install age brew tap ryantm/agenix brew install agenix # 验证安装 agenix --help age --help接下来收集团队成员的SSH公钥。公钥通常位于~/.ssh/id_ed25519.pub或~/.ssh/id_rsa.pub。让每个成员提供他们公钥文件的内容一串以ssh-xxx开头的文本。3.2 创建并配置 secrets.nix在你的项目根目录或一个专门的secrets目录下创建secrets.nix。# ./secrets/secrets.nix let # 将团队成员的公钥以变量形式定义在这里。 # 在实际项目中可以考虑将这些公钥放在一个单独的 nix 文件中引入以便复用。 user1 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJl... user1laptop; user2 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKj... user2desktop; # 生产服务器的主机密钥公钥通常位于 /etc/ssh/ssh_host_ed25519_key.pub productionServer ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPM... rootproduction; in { # 定义秘密文件‘database.env.age’允许 user1, user2 和 productionServer 解密。 database.env.age.publicKeys [ user1 user2 productionServer ]; api-key.age.publicKeys [ user1 productionServer ]; # 你可以为不同环境定义不同的秘密和授权。 # staging/api-key.age.publicKeys [ user1 user2 ]; }实操心得管理公钥列表是团队协作的关键。对于大型团队建议创建一个keys.nix文件导出所有成员的公钥变量然后在secrets.nix中import它。这比把几十个公钥硬编码在一个文件里要清晰得多。3.3 生成你的第一个加密秘密现在我们来创建加密的数据库连接字符串。# 进入secrets目录 cd secrets # 编辑并加密 database.env.age 文件 agenix -e database.env.age命令执行后你的默认编辑器如vim或nano会打开一个临时文件。在这个文件里你可以像写普通的.env文件一样输入秘密DB_HOSTproduction-db.internal DB_PORT5432 DB_NAMEmyapp DB_USERmyapp_user DB_PASSWORDSuperSecretPassword123!保存并退出编辑器后agenix会自动完成以下操作读取secrets.nix找到database.env.age对应的公钥列表[user1, user2, productionServer]。使用age工具用这些公钥加密你刚才输入的明文内容。将加密后的密文写入database.env.age文件。现在查看database.env.age你会看到一堆看似乱码的文本这就是被加密的内容。这个文件可以安全地提交到Git仓库。3.4 解密与使用秘密作为被授权的用户user1你想在本地开发时连接数据库需要解密这个文件。# 在 secrets 目录下 agenix -d database.env.age这会将解密后的明文内容输出到终端。通常我们会将其重定向到一个临时文件或直接通过管道传递给需要它的程序。# 解密并写入一个临时环境文件切勿提交此文件 agenix -d database.env.age /tmp/db.env # 使用 source 命令加载环境变量在bash/zsh中 source /tmp/db.env # 或者直接传递给程序 DB_CONNECTION_STRING$(agenix -d database.env.age) my_app重要注意事项解密操作依赖于本地的SSH私钥。确保你的SSH私钥路径是agenix期望的默认是~/.ssh/id_ed25519或~/.ssh/id_rsa。如果你的私钥在其他位置需要通过-i参数指定agenix -i ~/.ssh/my_key -d ...。私钥的权限必须正确如600否则age库可能会出于安全原因拒绝读取。如果你的私钥有密码agenix会通过SSH代理ssh-agent或提示你输入密码来获取。确保ssh-agent正在运行且你的密钥已添加ssh-add ~/.ssh/id_ed25519。3.5 在NixOS部署中自动化集成这是agenix最强大的场景。在你的NixOS服务器配置/etc/nixos/configuration.nix或flake的配置模块中添加如下模块# 首先引入 agenix 的 NixOS 模块。 # 如果你使用 Flakes在 inputs 中添加 agenix并在 outputs 中传递。 # 如果是经典配置可以通过类似的方式引入。 { config, pkgs, lib, ... }: { imports [ # 假设你将 agenix 作为 flake input 引入这里是其 nixos 模块 inputs.agenix.nixosModules.default ]; # 配置 agenix age.identityPaths [ /etc/ssh/ssh_host_ed25519_key ]; # 使用服务器主机密钥作为解密身份 # 也可以添加其他密钥路径如用于部署的专用密钥 # 定义秘密 age.secrets.database-env { # 加密文件的位置。这里假设你的配置和secrets在同一个git仓库中。 file ../secrets/database.env.age; # 解密后秘密将被放置在 /run/agenix/database-env # path 选项默认基于名称生成这里是可选的。 # owner 和 group 设置文件所有者 owner appuser; group appgroup; # 设置文件权限例如 0440 表示所有者和组可读 mode 0440; }; # 在你的服务配置中使用这个秘密 systemd.services.my-web-app { serviceConfig { # 将秘密文件作为环境变量文件加载 EnvironmentFile config.age.secrets.database-env.path; }; # ... 其他服务配置 }; }当你在这台服务器上运行sudo nixos-rebuild switch时NixOS构建过程会识别到age.secrets.database-env这个配置。找到对应的加密文件../secrets/database.env.age。使用age.identityPaths中指定的私钥这里是服务器SSH主机密钥尝试解密。因为服务器的公钥在secrets.nix的授权列表中解密成功。将解密后的内容写入/run/agenix/database-env并设置好指定的权限和所有者。你的服务my-web-app启动时会自动加载这个环境变量文件。整个过程完全自动化、声明式且无需在服务器上留下任何明文秘密。4. 高级技巧与安全最佳实践掌握了基础工作流后我们深入探讨一些高级用法和安全考量让你的agenix使用更上一层楼。4.1 多环境与分层秘密管理在复杂的项目中你可能有开发、测试、预发布、生产等多个环境每个环境的秘密不同。# secrets.nix let keys import ./keys.nix; # 集中管理所有公钥 in { # 开发环境秘密所有开发者都可解密 dev/database.env.age.publicKeys with keys; [ alice bob charlie ]; dev/redis.url.age.publicKeys with keys; [ alice bob charlie ]; # 生产环境秘密只有运维和服务器可解密 prod/database.env.age.publicKeys with keys; [ sysadmin productionServerA productionServerB ]; prod/api-key.age.publicKeys with keys; [ sysadmin productionServerA ]; # 共享秘密如内部服务的通用令牌 internal/auth-token.age.publicKeys with keys; [ alice bob charlie sysadmin productionServerA ]; }通过目录结构进行逻辑划分清晰明了。在NixOS配置中可以根据config.networking.hostName或其他条件变量动态选择加载哪个环境的秘密文件。4.2 使用age原生密钥进行机器间通信虽然SSH密钥是主要方式但age原生密钥在某些场景下更有优势。例如两个服务之间需要安全传输数据但它们没有SSH密钥对。# 生成一对 age 原生密钥 age-keygen -o key.txt # 这会输出公钥以 age1... 开头和私钥。私钥保存在 key.txt 中。将公钥age1...添加到secrets.nix的publicKeys列表中。私钥文件key.txt可以放在服务器上并通过age.identityPaths引入。原生age密钥没有密码保护更适合自动化场景但务必保证私钥文件本身的安全严格的文件权限可能结合全盘加密。4.3 密钥轮换与撤销安全策略要求定期轮换密钥。使用agenix这变得相对简单。成员离职从keys.nix或secrets.nix中移除该成员的公钥。重新加密所有秘密由于secrets.nix已更新你需要用新的公钥列表重新加密所有受影响的.age文件。可以写一个简单的脚本批量操作# 假设所有 .age 文件都在当前目录 for secret in *.age; do echo Re-encrypting $secret agenix -e $secret done这个过程需要当前仍有权限的成员或一个仍有效的密钥来执行因为agenix -e需要先解密旧文件再用新密钥列表加密。服务器密钥轮换如果服务器SSH主机密钥更换需要将新的公钥添加到secrets.nix并确保在旧密钥失效前完成所有秘密的重新加密和部署。踩坑记录密钥轮换最大的风险是“时间差”。如果移除一个密钥后没有立即重新加密并部署所有秘密那么持有旧密钥的人可能仍然可以解密缓存在本地的旧版本加密文件。因此轮换操作应计划在维护窗口内快速完成并确保所有仓库和部署渠道中的加密文件都已更新。4.4 备份与灾难恢复你的加密秘密和secrets.nix文件是安全的可以备份。但必须备份解密密钥SSH私钥如果所有授权成员的私钥都丢失那么加密数据将永久无法恢复。个人妥善备份你的SSH私钥例如使用密码管理器、加密的USB驱动器。团队考虑使用一个“紧急访问”密钥对。生成一对专用的age密钥将公钥添加到所有关键秘密的授权列表中将私钥打印出来纸备份或存储在离线硬件安全模块中放在安全的物理位置如保险箱。这个密钥仅在灾难恢复时使用。5. 常见问题排查与调试实录即使设计再精良在实际操作中也会遇到问题。这里记录了一些典型场景和解决方法。5.1 解密失败No matching keys found这是最常见的问题。错误信息表明你当前用于解密的私钥与加密该文件所使用的任何一个公钥都不匹配。$ agenix -d prod-secret.age Error: No matching keys found in identity files ...排查步骤检查当前使用的私钥agenix默认使用~/.ssh/id_ed25519和~/.ssh/id_rsa。用ssh-add -L查看当前ssh-agent中加载的公钥确认它是否在secrets.nix的授权列表里。检查secrets.nix确认你尝试解密的文件如prod-secret.age在secrets.nix中正确定义并且你的公钥确实在对应的publicKeys列表中。注意公钥字符串必须完全匹配包括末尾的注释邮箱。指定私钥路径如果你使用非默认路径的私钥必须用-i参数明确指定agenix -i /path/to/your/deploy_key -d prod-secret.age检查私钥格式age主要支持ssh-ed25519和ssh-rsa格式的密钥。如果你使用的是较旧的dsa或ecdsa密钥可能需要转换格式或使用age原生密钥。5.2 在CI/CD流水线中自动解密在GitLab CI、GitHub Actions等环境中你需要让运行器Runner能够解密秘密。方案使用部署密钥Deploy Key生成一个专用于CI的SSH密钥对不要设置密码。将公钥添加到项目的secrets.nix授权列表中。将私钥作为受保护的CI/CD变量如CI_DEPLOY_KEY存储在CI平台中。确保变量类型是File或妥善处理多行文本。在CI脚本中将私钥写入文件设置正确权限然后使用-i参数指定它。# .gitlab-ci.yml 示例片段 deploy: script: - | # 将变量中的私钥写入文件 echo $CI_DEPLOY_KEY deploy_key chmod 600 deploy_key # 使用该密钥解密 agenix -i deploy_key -d database.env.age .env # 使用解密后的秘密进行部署... # 务必在最后清理私钥文件 - rm -f deploy_key安全警告CI中的私钥没有密码保护因此必须严格控制该CI变量的访问权限并确保私钥文件在作业结束后被立即删除。5.3 处理大型二进制文件age和agenix主要用于加密文本文件。对于大型二进制文件如TLS证书、密钥库虽然可以工作但效率可能不是最优。age本身支持流式加密但对于集成在Nix构建中大文件可能会影响构建时间。建议对于非常大的二进制秘密考虑将其存储在专用的安全存储中如S3桶配合服务器端加密而在agenix中只存储访问该存储所需的凭证如一个预签名的URL或访问密钥。这样既安全又不影响Nix构建的效率和确定性。5.4 调试查看加密文件的收件人信息有时你需要确认一个.age文件到底是用哪些公钥加密的。age工具本身提供了这个功能age -d -i /dev/null your-secret.age 21 | head -20这个命令会尝试用空密钥解密必然会失败但错误信息中会列出该加密文件的所有收件人Recipient的指纹或公钥片段。你可以将这些信息与secrets.nix中的公钥列表进行比对验证加密配置是否正确。5.5 NixOS构建时解密失败在nixos-rebuild switch时如果遇到解密失败错误信息可能不够清晰。检查age.identityPaths确保配置中指定的私钥路径存在且可读。对于服务器主机密钥通常是/etc/ssh/ssh_host_ed25519_key。确保Nix构建进程以root身份运行有权限读取该文件。手动测试解密登录到服务器尝试手动使用指定的身份文件解密sudo agenix -i /etc/ssh/ssh_host_ed25519_key -d /path/to/secret.age如果手动解密成功但Nix构建失败可能是路径引用问题或构建环境差异。查看详细的Nix构建日志使用nixos-rebuild switch --show-trace或journalctl -u nix-daemon来获取更详细的错误信息。确认公钥匹配确保服务器上/etc/ssh/ssh_host_ed25519_key.pub的内容与你放在secrets.nix中的公钥字符串完全一致。主机密钥有时会重新生成需要更新secrets.nix。我个人在将agenix引入团队工作流的初期最大的挑战是统一大家对“声明式秘密管理”的认知。一旦跨过最初的学习曲线尤其是在一次紧急的密钥撤销事件中我们仅用几分钟就更新了secrets.nix并重新加密了所有文件而无需手动登录十几台服务器团队立刻认识到了它的价值。它带来的不仅是安全更是一种秩序和可审计性。对于任何使用NixOS或希望在Git中安全管理配置的团队agenix都是一个值得深入研究和采用的工具。