Compare commits

...

276 Commits
0.9 ... main

Author SHA1 Message Date
Nathan Henrie 8d37c5bdea
Merge pull request #259 from hansemschnokeloch/patch-1
Fix typo
2024-05-09 15:32:35 -06:00
hansemschnokeloch 63a57d8dfb
Fix typo 2024-05-09 22:25:29 +02:00
Jörg Thalheim 07479c2e73
update link to nixos wiki (#258) 2024-05-07 10:12:37 -07:00
Ryan Mulligan 24a7ea3905
Merge pull request #256 from spectre256/main
fix: allow for newlines in keys
2024-04-26 05:59:12 -07:00
Ellis Gibbons 2c1d1fb134
fix: allow for newlines in keys 2024-04-12 17:50:07 -04:00
Cole Helbling 1381a759b2
Merge pull request #254 from oluceps/fix-doc
doc: fix wrong `ssh-keyscan` usage
2024-04-02 10:31:00 -07:00
oluceps 3fd98a2c3b
doc: fix wrong ssh-keyscan usage 2024-04-03 01:00:02 +08:00
Ryan Mulligan 8cb01a0e71
Merge pull request #244 from kraem/fix/rage_to_age_docs
fix: update docs for 5c1198a
2024-02-13 05:27:47 -08:00
kraem 1f62cef426 fix: update docs for 5c1198a 2024-02-07 08:48:49 +01:00
Ryan Mulligan 417caa847f
Merge pull request #232 from ryantm/rtm-12-23-test
dev: reland add direct tests for agenix
2023-12-24 08:04:03 -08:00
Ryan Mulligan a23aa271be dev: reland add direct tests for agenix
Why
===
* We'd like some tests for the CLI
* Last time we tried it failed on macos

What changed
===
* This time, we try to create the temp diretory in a way that works
with macos too
2023-12-23 14:47:15 -08:00
Ryan Mulligan bc24f2e510 Revert "Revert "contrib: add direct tests for agenix ""
This reverts commit 08dc5068e6.
2023-12-23 14:43:03 -08:00
Ryan Mulligan 457669db42
Merge pull request #230 from ryantm/rtm-12-20-age
feat: switch from rage to age
2023-12-23 14:40:41 -08:00
Nathan Henrie 6ce42cc768 Fix CI for darwin
nix-darwin is detecting `/etc/nix/nix.conf` from the
cachix/install-nix-action and refusing to overwrite it, failing the
nix-darwin activation and therefore the rest of CI.

This commit `mv`s the existing `nix.conf` to avoid the above, and then
adds support for new-style nix commands and flakes to the nix-darwin
configuration to allow their subsequent use in CI.

It also removes the nix config from the `cachix/install-nix-action`
step, which was duplicated effort since we are blowing it away with
nix-darwin anyway.

Relevant:

- https://github.com/LnL7/nix-darwin/issues/149
2023-12-23 14:10:44 -08:00
Ryan Mulligan 23d4d5d291 maybe this fixes darwin checks? 2023-12-23 14:10:06 -08:00
Ryan Mulligan b6aa6180db test removing installer 2023-12-23 14:10:06 -08:00
Ryan Mulligan 58017c0c93 update inputs 2023-12-23 14:10:06 -08:00
Ryan Mulligan bd86c06961 fix doc build 2023-12-23 14:10:02 -08:00
Ryan Mulligan eb3b5cf4fd update nixpkgs 2023-12-23 14:09:16 -08:00
Ryan Mulligan 5c1198a352 feat: switch from rage to age
Why
===
* Someone said age works better with password protected keys,
requiring entering the password less often.
* We switched to rage from age in
07ce686870
because it was limiting recipients to 20. This was fixed
https://github.com/FiloSottile/age/issues/139

What changed
===
* Switch from rage back to age (the reference implementation) in all
the spots
* Update the docs to show how to switch back to Rage
* Skip keys that are empty files, which fixes the integration test.
2023-12-23 14:09:16 -08:00
Ryan Mulligan 9bc80dc4ce
Merge pull request #229 from ryantm/rtm-12-20-flake
dev: remove i686 support; simplify flake
2023-12-23 14:08:24 -08:00
Ryan Mulligan d0d4ad5be6
Merge pull request #231 from ryantm/revert-163-rtm-2-21-recursive-nix
Revert "contrib: add direct tests for agenix "
2023-12-22 07:48:36 -08:00
Ryan Mulligan 08dc5068e6
Revert "contrib: add direct tests for agenix " 2023-12-22 07:48:06 -08:00
Ryan Mulligan 17090d105a
Merge pull request #163 from ryantm/rtm-2-21-recursive-nix
contrib: add direct tests for agenix
2023-12-20 13:19:59 -08:00
Ryan Mulligan 097aa18b59 contrib: add direct tests for agenix
These tests are MUCH faster than the NixOS tests.
2023-12-20 13:06:57 -08:00
Ryan Mulligan 344f985526 dev: remove i686 support; simplify flake
Why
===
* flake.nix had a lot of almost redundant lines in it.
* i686 support is probably being dropped soon in nixpkgs
https://github.com/NixOS/nixpkgs/pull/266164

What changed
==
* Add new input nix-systems/default which represents the default
systems typically used in the Nix community
* Define and use an eachSystem function that simplifies the flake.nix file
2023-12-20 08:56:05 -08:00
Ryan Mulligan 564595d0ad version 0.15.0 2023-12-20 08:33:16 -08:00
Ryan Mulligan b7e0494b10
Merge pull request #224 from SamueleFacenda/SamueleFacenda-change-keys-functions
Update keys functions in agenix.sh
2023-12-20 08:17:38 -08:00
Samuele Facenda 9d3b37a117 fix: update keys functions in agenix.sh
The functions was always called with `$FILE` as first argument, but inside the argument is ignored. This change doesn' have any impact, but can solve problems caused by the keys function called with an argument different from `$FILE`
2023-12-20 08:08:56 -08:00
Ryan Mulligan 93cec0ce6e dev: add security label category 2023-12-20 08:03:06 -08:00
Ryan Mulligan 221a1f22e5 dev: add release-drafter 2023-12-20 07:52:23 -08:00
Ryan Mulligan 6cb7cd66c2
Merge pull request #221 from CobaltCause/edit-mkdir-p
create leading directories if they don't exist
2023-12-20 07:42:34 -08:00
Ryan Mulligan 13ac9ac6d6
Merge pull request #176 from shivak/patch-1
only backup cleartext file if it exists
2023-11-28 16:08:36 -08:00
Shiva Kaul 4c48606094 only backup cleartext file if it exists
Avoids complaints from `cp` about nonexistent files.
2023-11-28 16:05:48 -08:00
Charles Hall 65fe5959c3
create leading directories if they don't exist
This works for files without directories too because `dirname` prints
`.` in that case.
2023-11-03 15:25:24 -07:00
Charles Hall 05591973d7
use named variable instead of numbered one 2023-11-03 14:53:33 -07:00
Ryan Mulligan daf42cb35b
Merge pull request #208 from ryantm/revert-206-feature/remove-trailing-newlines-from-keys
Revert "feat: remove empty newlines from jq query"
2023-10-08 07:32:09 -07:00
Ryan Mulligan dbc533ddc2
Revert "feat: remove empty newlines from jq query" 2023-10-08 07:31:54 -07:00
Ryan Mulligan e2f339274d
Merge pull request #206 from timhae/feature/remove-trailing-newlines-from-keys
feat: remove empty newlines from jq query
2023-10-08 05:25:24 -07:00
Tim Häring b5fa96a90e
feat: remove empty newlines from jq query
When you do not have your pubkeys in your `secrets.nix` verbatim as
string but read them from file like this: `desktop1 = builtins.readFile
./desktop1.pub;`, you will end up with empty newlines in the resulting
list of keys, which will add `--recipient=''` arguments to your age
call, failing the call.
2023-10-08 14:18:54 +02:00
Ryan Mulligan 1f677b3e16
Merge pull request #202 from WillPower3309/main
fix: add --strict nix-instantiate to support builtins.readFile
2023-09-22 05:13:16 -07:00
William McKinnon 115e561054 fix: add --strict nix-instantiate to support builtins.readFile 2023-09-22 01:32:46 -04:00
Ryan Mulligan 7f9dfa309f
Merge pull request #148 from n8henrie/sed_vs_jq
contrib: use jq instead of sed
2023-09-21 16:33:52 -07:00
Nathan Henrie da763b2c4b Don't need concatStringSep if using jq to parse json arrays 2023-09-15 16:22:30 -06:00
Nathan Henrie eb1386f3b2 Use jq instead of sed 2023-09-15 11:56:22 -06:00
Ryan Mulligan 572baca9b0
Merge pull request #199 from n8henrie/fix-darwin-ci
fix: update cachix installer to fix darwin CI
2023-09-15 09:02:43 -07:00
Nathan Henrie b76899f4c1 Update nix installer
Fixes https://github.com/ryantm/agenix/issues/198
2023-09-15 08:26:02 -06:00
Nathan Henrie 7f30f9b4b3 Revert "dev: try switching to determinate systems installer action"
This reverts commit 2ed2dc7582.
2023-09-15 07:53:36 -06:00
Nathan Henrie da5d6f05f9
Merge pull request #195 from Eisfunke/fix-home-shellcheck
fix(home): shellcheck failure for fixed secretsDir
2023-09-15 07:40:28 -06:00
Ryan Mulligan 20deb735cc
Merge pull request #197 from ryantm/rtm-9-14-try-to-fix-ci
dev: try switching to determinate systems installer action
2023-09-14 16:42:44 -07:00
Ryan Mulligan 2ed2dc7582 dev: try switching to determinate systems installer action 2023-09-14 16:37:58 -07:00
Ryan Mulligan 54693c91d9 version 0.14.0 2023-09-14 16:20:33 -07:00
Ryan Mulligan 7d39a26d73
Merge pull request #196 from ryantm/ryantm-patch-1
Create flakehub-publish-tagged.yml
2023-09-14 16:19:55 -07:00
Ryan Mulligan 1698ed385d
Create flakehub-publish-tagged.yml 2023-09-14 16:19:37 -07:00
Nicolas Lenz fe4f564f13
fix(home): shellcheck failure for fixed secretsDir 2023-09-09 16:46:53 +02:00
Ryan Mulligan d8c973fd22
Merge pull request #192 from malteneuss/extend_documentation
Extend documentation to make it more NixOS beginner friendly.
2023-07-24 15:01:18 -05:00
malteneuss 91220a701d
Rephrase cli app summary 2023-07-24 21:51:25 +02:00
malteneuss 2bee5c988c
Extend tutorial section 2023-07-16 22:40:26 +02:00
malteneuss 1d7fd15690
Extend flake install section 2023-07-16 21:34:50 +02:00
malteneuss 6d20bf81f8
Fix intro indentation 2023-07-16 21:23:10 +02:00
malteneuss b91dfbaf76
Fix indentation 2023-07-16 20:17:20 +02:00
malteneuss 78733d6d09
Make intro section more beginner friendly 2023-07-16 20:12:02 +02:00
Ryan Mulligan 0d8c5325fc
Merge pull request #191 from linj-fork/fix-doc
doc: fix defaultText and description
2023-07-14 06:28:38 -05:00
Lin Jian 6e8a48c2dc
doc: fix nixos option format in descriptions 2023-06-27 00:06:58 +08:00
Lin Jian 0d94960783
doc: fix defaultText by adding literalExpression
I also remove an unnecessary defaultText and fix a typo.
2023-06-27 00:06:39 +08:00
Ryan Mulligan db5637d10f
Merge pull request #185 from Scrumplex/fix-shellcheck-warning
Disable shellcheck warning about impossible comparison
2023-05-15 05:29:13 -07:00
Sefa Eyeoglu 72205a86ca
Add test for custom secret paths for HM
Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2023-05-12 20:15:32 +02:00
Sefa Eyeoglu 758cdc98f4
Disable shellcheck warning about impossible comparison
This shellcheck warning occurs when setting a path for a secret using
the home-manager module.

Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2023-05-12 20:15:30 +02:00
Ryan Mulligan 92197270a1
Merge pull request #180 from ambroisie/add-home-manager
Add home-manager module
2023-05-11 21:38:43 -07:00
Nathan Henrie 6b4ff3d191 Check darwin home-manager test in CI 2023-05-06 14:58:01 +01:00
Nathan Henrie 50743bd117 Add darwin tests for home-manager module 2023-05-06 14:39:48 +01:00
Nathan Henrie 19bf5a20d8 Clean-up Darwin integration test 2023-05-06 14:18:17 +01:00
Nathan Henrie 3fbc22fe43 Install user keys in Darwin tests 2023-05-06 14:18:17 +01:00
Bruno BELANYI 0155c5710e Test home-manager module 2023-05-06 14:18:17 +01:00
Bruno BELANYI 1f43d94d52 Add home-manager input 2023-05-06 14:18:17 +01:00
Bruno BELANYI 9274b82816 Add home-manager module
This is to update and fix the issues I saw in [1] and [2].

Using a service definition instead of an activation script should
resolve the issue about the secrets disappearing after rebooting.

Removed the `user` and `group` option as they do not make sense to me
for a home-manager module, which should target a single user. They can
always be added back if somebody comes screaming.

This is somewhat modeled after sops-nix's own module [3].

[1]: https://github.com/ryantm/agenix/pull/58/
[2]: https://github.com/ryantm/agenix/pull/109
[3]: https://github.com/Mic92/sops-nix/blob/master/modules/home-manager/sops.nix
2023-05-06 14:18:17 +01:00
Cole Helbling 2994d002dc
Merge pull request #179 from winny-/patch-1
doc: missing space
2023-04-21 11:17:59 -07:00
Ryan Mulligan 0e3a237c5a
Merge pull request #175 from whentze/fix-decrypt-truncating
fix truncated output when decrypting a large file to stdout via -d
2023-04-21 07:28:48 -07:00
Winston (Winny) Weinert 8722cf94f1
doc: missing space 2023-04-20 18:50:12 -05:00
Nathan Henrie e64961977f
Merge pull request #155 from ryantm/rtm-2-19-doc-no-darwin
doc: how to skip the Darwin input
2023-03-31 10:49:20 -06:00
Wanja Hentze 40550f0619 fix truncated output when decrypting a large file to stdout via -d
rage intentionally truncates large output when writing to stdout:
55e52c252b/age/src/cli_common/file_io.rs (L219)
but if told to write to "-" instead, it will not truncate:
55e52c252b/age/src/cli_common/file_io.rs (L312)
2023-03-14 18:53:32 +01:00
Ryan Mulligan 03b51fe8e4
Merge pull request #174 from ryantm/rm-3-4-doc
doc: actually fix github pages deploy
2023-03-04 14:42:46 -08:00
Ryan Mulligan b1d6d764e1 doc: actually fix github pages deploy 2023-03-04 14:41:59 -08:00
Ryan Mulligan 1abf0ade92
Merge pull request #173 from ryantm/rm-3-4-doc
doc: try a slightly different format for github action
2023-03-04 13:07:34 -08:00
Ryan Mulligan 2fb0a74be3 doc: try a slightly different format for github action 2023-03-04 13:06:51 -08:00
Ryan Mulligan 36986c8fed
Merge pull request #172 from ryantm/rm-3-4-doc
doc: try to fix doc ci
2023-03-04 12:05:30 -08:00
Ryan Mulligan 119fac65b4 doc: try to fix doc ci 2023-03-04 12:04:58 -08:00
Ryan Mulligan 6a2757101d
Merge pull request #170 from ryantm/rtm-2-26-mmdoc
doc: add new doc website
2023-03-04 10:46:20 -08:00
Ryan Mulligan 657789137c doc: add new doc website
* use mmdoc
* add github pages action to auto publish
* do not edit README for now, will follow up with a commit directs
people to the doc site
2023-03-04 10:34:29 -08:00
Ryan Mulligan 4828951d9d
Merge pull request #171 from ryantm/revert-169-rm-2-26-identity-storepath
Revert "fix: disallow Nix store paths in age.identityPaths option"
2023-02-26 15:22:22 -08:00
Ryan Mulligan b67873854d
Revert "fix: disallow Nix store paths in age.identityPaths option" 2023-02-26 15:11:56 -08:00
Ryan Mulligan faf978f7f3
Merge pull request #169 from ryantm/rm-2-26-identity-storepath
fix: disallow Nix store paths in age.identityPaths option
2023-02-26 13:45:03 -08:00
Ryan Mulligan 1141c36c26 fix: disallow Nix store paths in age.identityPaths option 2023-02-26 09:03:17 -08:00
Ryan Mulligan 9225d56306
Merge pull request #168 from n8henrie/issue_165_docs
Expand explanation that identityPaths should be strings
2023-02-26 08:54:58 -08:00
Nathan Henrie 37dcc5f5e7 Expand explanation that identityPaths should be strings 2023-02-24 11:17:12 -07:00
Ryan Mulligan 833f87c8ff
Merge pull request #164 from whentze/decrypt-only-fix-binary
fix -d/--decrypt-only not working correctly for binary data
2023-02-24 06:01:20 -08:00
Wanja Hentze 7dae15b7bc fix -d/--decrypt-only not working correctly for binary data
I had first used `printf` for outputting the data,
but that breaks if the secret itself contains null bytes.

One could fix this by using e.g. `cat`, but looking a bit more at the code
I realized that in the -d case we never need to `mktemp` at all and can
just ask `age` to write directly to stdout by not setting -o.
2023-02-24 09:00:48 +01:00
Ryan Mulligan c2a71c83c7
Merge pull request #158 from whentze/decrypt-only
add -d/--decrypt option to decrypt a file to stdout
2023-02-22 20:25:46 -08:00
muvlon 9cf1967039 feature: add -d/--decrypt option to decrypt a file to stdout 2023-02-22 19:20:58 -08:00
Ryan Mulligan 2d735d6518
Merge pull request #162 from ryantm/rtm-2-21-stop-packaging-rage
contrib: stop packaging rage
2023-02-22 09:07:10 -08:00
Ryan Mulligan 2c0ae7d44f contrib: stop packaging rage
We don't need to package rage anymore, since all the latest maintained
versions of Nix have versions higher than what we need.
2023-02-21 20:33:19 -08:00
Ryan Mulligan 0c50bbe60e
Merge pull request #161 from n8henrie/warnings-to-stderr
Output user-facing warnings to stderr instead of stdout
2023-02-21 15:17:43 -08:00
Nathan Henrie 283c178469 Add warn and err helpers, use `diff -q` 2023-02-21 12:46:44 -07:00
Nathan Henrie d84a99d0b8 Redirect user-directed warnings to stderr 2023-02-21 12:42:19 -07:00
Nathan Henrie 5f66c8aa77
Merge pull request #154 from ryantm/rtm-2-19-pipe
feature: pipe cleartext into agenix -e
2023-02-20 09:30:39 -07:00
Ryan Mulligan 53da86e976
Merge pull request #156 from mputz86/main
Make isDarwin check more robust
2023-02-20 06:45:27 -08:00
Matthias Putz ec66ebe0ee Make isDarwin check more robust 2023-02-20 13:47:48 +01:00
Ryan Mulligan b0721be0c6 doc: how to skip the Darwin input 2023-02-19 15:12:18 -08:00
Ryan Mulligan 344c8e41d2 feature: pipe cleartext into agenix -e
If STDIN is not interactive, change EDITOR to `cp /dev/stdin`.

fixes #33
2023-02-19 10:20:07 -08:00
Ryan Mulligan 2c56a93426
Merge pull request #153 from ryantm/rtm-2-18-test-docs
contrib: add instructions for running the tests
2023-02-18 21:50:37 -08:00
Ryan Mulligan c602dc4ffb contrib: add instructions for running the tests 2023-02-18 18:37:43 -08:00
Nathan Henrie 78a22dbc0d
Merge pull request #152 from ryantm/rtm-2-18-fix-bogus-id-rsa 2023-02-18 16:00:27 -07:00
Ryan Mulligan 16c6ccef09 test: simplify and speed up editor tests 2023-02-18 12:52:13 -08:00
Ryan Mulligan ec396f7a76 fix: if an identity is specified, don't use the default ones
fixes #151
2023-02-18 11:55:58 -08:00
Ryan Mulligan e4f0dcc8d3 test: add tests for editing
* regular editing
* in presence of bogus id_rsa file
2023-02-18 11:54:48 -08:00
Ryan Mulligan de657061b1
Merge pull request #150 from n8henrie/expand_tests
Expand tests
2023-02-16 17:58:21 -08:00
Nathan Henrie 0b5c4b8c8f Test rekeying via agenix CLI
This test copies the example `secrets.nix` and age files and uses the
user key to rekey them. It compares the hash before and after to ensure
that the age file is actually being changed.
2023-02-16 14:19:42 -07:00
Nathan Henrie 9e361f8b39 Install agenix CLI 2023-02-16 14:19:28 -07:00
Nathan Henrie 0efac6bcf0 Add user key, since it has access to all 3 secrets 2023-02-16 14:18:54 -07:00
Nathan Henrie effb43cb63 Use new-style tests 2023-02-13 11:50:59 -07:00
Ryan Mulligan ea17cc71b4
Merge pull request #139 from ryantm/rtm-1-29-cli-test
contrib: use mkDerivation for agenix cli
2023-02-11 14:18:23 -08:00
Ryan Mulligan d0b75ddf9a contrib: use mkDerivation for agenix cli
* use mkDerivation
* separate shell code in own file
* use shellcheck to lint shell code
* remove rage version check since rage is greater than 0.5.0 on all
  maintained nixpkgs
2023-02-11 13:18:31 -08:00
Ryan Mulligan 6053c559c5
Merge pull request #146 from n8henrie/issue_143
Skip missing or unreadable keys
2023-02-11 08:54:07 -08:00
Nathan Henrie 37c7297956 Skip missing or unreadable keys 2023-02-11 07:34:06 -07:00
Nathan Henrie 578794f528 Test with nonexisting key 2023-02-11 07:31:09 -07:00
Ryan Mulligan b7ffcfe77f
Merge pull request #141 from n8henrie/nix-darwin-support
feature: try to add nix-darwin support
2023-01-31 06:45:55 -08:00
Nathan Henrie d7fd31756e Remove activation scripts again 2023-01-30 15:52:05 -07:00
Nathan Henrie 6ec0b0f7c7 Revert to hdiutil for older macos compatibility, be explicit about the weird number after ram:// 2023-01-30 15:51:52 -07:00
Nathan Henrie 9779a98f1e Testing for CI -- revert "Remove activation scripts"
This reverts commit 4c315d9683.
2023-01-30 15:33:50 -07:00
Nathan Henrie 4b2b6fa111 Simplify removal of trailing spaces 2023-01-30 14:37:15 -07:00
Nathan Henrie 4c315d9683 Remove activation scripts 2023-01-30 14:21:49 -07:00
Nathan Henrie 9b94b43971 format 2023-01-30 14:21:42 -07:00
Nathan Henrie c69689da58 Use diskutil for more convenient sizes, strip trailing tabs 2023-01-30 14:21:33 -07:00
Nathan Henrie b818ac2e7d fmt 2023-01-30 09:18:56 -07:00
Nathan Henrie 019784cb7e Give volume a name 2023-01-30 09:06:59 -07:00
Nathan Henrie 8867c12d72 Cleanup, improve readability 2023-01-30 09:06:39 -07:00
Nathan Henrie 4532604741 Silence output 2023-01-30 09:06:03 -07:00
Nathan Henrie 351e874918 Try to add nix-darwin support to agenix
Merges work by @montchr, @cmhamill, and @rtimush and rebases on main.

- fixes https://github.com/ryantm/agenix/issues/60
- fixes https://github.com/ryantm/agenix/issues/120
- closes https://github.com/ryantm/agenix/pull/107
2023-01-29 16:41:49 -07:00
Ryan Mulligan 49798e535e
Merge pull request #140 from ryantm/rtm-1-29-doc
doc: collapse installation sections
2023-01-29 14:20:34 -08:00
Ryan Mulligan c695ebce9a doc: collapse installation sections
as suggested in #133
2023-01-29 14:13:45 -08:00
Ryan Mulligan 6d3a415637
Merge pull request #137 from ryantm/rtm-1-30-nix-format
contrib: format with Alejandra
2023-01-29 11:51:44 -08:00
Ryan Mulligan 16bef569f4 contrib: format Nix code with Alejandra 2023-01-29 10:57:51 -08:00
Ryan Mulligan 99e0963743 contrib: use Alejandra as formatter 2023-01-29 10:57:51 -08:00
Ryan Mulligan bf537f5b72
Merge pull request #136 from ryantm/rtm-1-29-flake-format
feature: update flake output format and docs
2023-01-29 10:54:24 -08:00
Ryan Mulligan 64b0574514 feature: update flake output format and docs 2023-01-29 10:44:19 -08:00
Cole Helbling 42d371d861
Merge pull request #131 from erikarvstedt/fix-test 2023-01-09 13:59:21 -08:00
Erik Arvstedt 822f71b8d8 test: fix type error
Fix this mypy typcheck error in the test builder:

  testScriptWithTypes:52: error: Argument 1 to "wait_until_tty_matches" of
  "Machine" has incompatible type "int"; expected "str"
      system1.wait_until_tty_matches(2, "login: ")

This makes the test succeed again.
2023-01-09 11:25:24 +01:00
Ryan Mulligan a630400067
Merge pull request #127 from montchr/nixpkgs-update
Track `nixos-unstable` channel as `nixpkgs`
2022-10-15 14:46:35 -07:00
Chris Montgomery ffbca4ae7e
fix: track `nixos-unstable` channel as `nixpkgs` 2022-10-15 13:41:44 -04:00
Ryan Mulligan ff2dc4fb88
Merge pull request #125 from montchr/gitignore-result
chore: add nix build result path to gitignore
2022-10-15 09:21:40 -07:00
Chris Montgomery a8ccd5bfa8
chore: add nix build result path to gitignore 2022-10-15 12:10:02 -04:00
Ryan Mulligan 6acb1fe5f8 version 0.13.0 2022-09-25 14:22:43 -07:00
Ryan Mulligan 78d871220f contrib: fix _incr_version script 2022-09-25 14:22:15 -07:00
Ryan Mulligan d51af86302 contrib: fix mode of release helper scripts 2022-09-25 14:15:41 -07:00
Ryan Mulligan a4ad67c46e contrib: add maintainer release helper scripts 2022-09-25 14:14:37 -07:00
Ryan Mulligan 84f0dc0a4f doc: add version to agenix command 2022-09-25 14:14:27 -07:00
Ryan Mulligan edf0d09012 doc: mention secrets.nix is not imported into a NixOS configuration 2022-09-25 12:57:30 -07:00
Ryan Mulligan c96da5835b doc: stop suggesting looking at the code now that Reference exists 2022-09-03 14:48:36 -07:00
Ryan Mulligan 68a8bc2951 doc: fix tutorial syntax 2022-09-03 14:47:54 -07:00
Ryan Mulligan 5d802d251c doc: improve tutorial by showing how to use the secret path 2022-09-03 14:47:04 -07:00
Ryan Mulligan d13c6d3bb7 doc: fix syntax of example 2022-09-03 14:42:11 -07:00
Ryan Mulligan 7ebd7d741d doc: add .path reference, and incorporate some sections into the Reference 2022-09-03 14:40:00 -07:00
Ryan Mulligan 3c34edaf65 Revert "doc: merge Use other implementations into Reference"
This reverts commit b352e6b70f.
2022-09-03 11:54:29 -07:00
Ryan Mulligan b352e6b70f doc: merge Use other implementations into Reference 2022-09-03 11:53:16 -07:00
Ryan Mulligan e05a49ee30 doc: make ageBin override a valid module 2022-09-03 11:50:45 -07:00
Ryan Mulligan bce59868a2 doc: add reference section 2022-09-03 11:46:45 -07:00
Ryan Mulligan 9f136ecfa5
Merge pull request #119 from ryantm/order
feature: combine root and nonroot secret install; delay chowning
2022-09-01 08:42:56 -07:00
Ryan Mulligan f86b56229b feature: combine root and nonroot secret install; delay chowning 2022-07-10 11:47:58 -07:00
Jeroen Simonetti fe206b4306
[module] change operation order
Change the order of operations to:

1. create new generation
2. decrypt secrets into new generation
3. symlink and remove old generation/secrets

Signed-off-by: Jeroen Simonetti <jeroen@simonetti.nl>
2022-07-10 19:12:55 +02:00
Ryan Mulligan 7e5e58b98c
Merge pull request #114 from timhae/newlines
remove empty lines in recipient keys file
2022-05-16 07:46:50 -07:00
Tim Häring 0e2fb13ecf remove newlines in recipient keys file
if not removed, empty lines will be added to the final encryption
command as --recipient '' which causes the command to fail with invalid
recipient ''
2022-05-15 20:03:52 +02:00
Ryan Mulligan 0d5e59ed64
Merge pull request #110 from ryantm/doc
doc: add readFile anti-pattern
2022-04-02 16:34:17 -07:00
Ryan Mulligan f2ff19dc81 doc: add readFile anti-pattern 2022-04-02 15:11:48 -07:00
Ryan Mulligan 764c975e74
Merge pull request #106 from ryantm/warnings
feature: warn about missing files
2022-03-09 09:03:24 -08:00
Ryan Mulligan 25b5bcfce9
Merge pull request #80 from felixscheinost/add-aarch64-darwin-package
Add package for aarch64-darwin
2022-03-08 20:27:43 -08:00
Ryan Mulligan 1a4643b779 feature: warn about missing files
rage itself does not have good error messages when files are missing,
so add some of our own checks and warnings.
2022-03-08 08:00:43 -08:00
Ryan Mulligan 297cd58b41 doc: add Community and Support section 2022-02-28 19:34:22 -08:00
Ryan Mulligan bad5a7be94 doc: use default nixosModule in NixOS flake 2022-02-28 19:29:39 -08:00
Ryan Mulligan 7309a8fc1f
Merge pull request #105 from luishfonseca/patch-1
Add default NixOS module to flake
2022-02-28 10:16:19 -08:00
Luís Fonseca 9316abd9f5
Add default NixOS module to flake
This adds a “default” NixOS module in flake.nix. This makes using this in flakes a little less verbose and repetitive.

Before this change:

```nix
nixpkgs.lib.nixosSystem {
  modules = [
    ./configuration.nix
    agenix.nixosModules.age
  ];
}
After this change:

```nix
nixpkgs.lib.nixosSystem {
  modules = [
    ./configuration.nix
    agenix.nixosModule
  ];
}
```
2022-02-28 17:38:21 +00:00
Ryan Mulligan b4ab630f19
Merge pull request #103 from Pacman99/configure-secretsDir
modules/age: add option for secrets directory
2022-02-22 13:42:35 -08:00
Parthiv Seetharaman 85bd9d01ad modules/age: add option for secrets directory 2022-02-21 15:20:05 -08:00
Ryan Mulligan a17d1f3055
Merge pull request #98 from nixinator/nixinator-just-spelling
correct readme spelling thats all
2022-02-02 14:42:37 -08:00
nixinator 3fbac9275f correct readme spelling thats all 2022-02-02 21:53:46 +00:00
Ryan Mulligan 08b9c96878
Merge pull request #93 from jtojnar/create-run
Ensure /run is created before mounting secrets
2022-01-07 09:24:25 -08:00
Jan Tojnar 35ecba5704 Do not try to create /run/agenix in when installing secrets
That is a job for agenixMountSecrets, which should have already
created a symlink there so the directory creation attempt would
fail anyway.
2022-01-06 22:55:10 +01:00
Jan Tojnar 26edd03a5a Ensure /run is created before mounting secrets
Otherwise /run/agenix might disappear if specialfs is toposorted
between agenixMountSecrets and agenixRoot.

Fixes: https://github.com/ryantm/agenix/issues/92
2022-01-06 22:50:56 +01:00
Ryan Mulligan c5558c88b2 doc: fix niv CLI installation instructions 2021-12-29 10:20:00 -08:00
Ryan Mulligan c882982544
Merge pull request #88 from ryantm/readme
doc: table of contents and better installation instructions
2021-12-29 10:18:18 -08:00
Ryan Mulligan d00ce39997 doc: remove old NixOS version compatibility notice 2021-12-29 10:17:14 -08:00
Ryan Mulligan 81ebe4f1f4 doc: table of contents and better installation instructions 2021-12-29 10:15:09 -08:00
Ryan Mulligan 57806bf7e3
Merge pull request #82 from ryantm/identitypaths
feature: rename age.sshKeyPaths to age.identityPaths
2021-12-06 16:37:36 -08:00
Felix Scheinost 42a250cafa Add package for aarch64-darwin
flake.lock previously included a "indirect" reference to nixpkgs.

I am not sure what this means but I added `inputs.nixpkgs` and updated nixpkgs because this old version of nixpkgs didn't have any support for aarch64-darwin at all.

Now on a aarch64-darwin I can type `nix build` and get a working version of agenix.
2021-12-06 09:11:34 +01:00
Ryan Mulligan dfb2e7e591 feature: rename age.sshKeyPaths to age.identityPaths
implements #66
2021-12-05 16:05:06 -08:00
Ryan Mulligan c53ac31e44
Merge pull request #81 from chuangzhu/agebin
Allow customizing ageBin
2021-12-05 15:53:34 -08:00
Chuang Zhu d85abe9f12
update README 2021-12-06 07:18:47 +08:00
Chuang Zhu c2f6bd077c
allow customizing ageBin 2021-12-06 07:08:18 +08:00
Ryan Mulligan 52ea2f8c32
Merge pull request #78 from mausch/patch-1
Fix reference to module in docs
2021-11-30 16:38:58 -08:00
Mauricio Scheffer 4625cd526f
Fix reference to module in docs 2021-11-30 23:08:57 +00:00
Ryan Mulligan f85eea0e29
Merge pull request #77 from Sohalt/main
update option descriptions
2021-11-24 14:43:10 -08:00
sohalt ed0d9ef01a update option descriptions 2021-11-24 18:00:28 +01:00
Ryan Mulligan a0e9ca505c
Merge pull request #73 from ymarkus/readme
README: clarify that 'config' has to be prefixed
2021-11-22 16:06:15 -08:00
Yannick Markus 8bf3896818
README: clarify that 'config' has to be prefixed 2021-11-21 15:13:56 +01:00
Ryan Mulligan 4a93de2beb
readme: master -> main 2021-11-20 17:30:45 -08:00
Ryan Mulligan cb0fe60ff1
Merge pull request #72 from oslerw/patch-1
Install instructions: master -> main
2021-11-20 17:12:49 -08:00
William Osler fc8272d31c
master -> main
Fix installation instructions for channel installation, now that the default branch name has changed.
2021-11-20 16:29:27 -08:00
Ryan Mulligan 4fefd7cfff
Merge pull request #71 from ryantm/fix-non-root-secrets
fix: make non-root secrets accessible again
2021-11-20 12:23:07 -08:00
Ryan Mulligan 5ff75b48b4 fix: make non-root secrets accessible again
fixes #69
2021-11-20 12:19:52 -08:00
Ryan Mulligan b8e873bc23 ci: split linux and macos
That wasn't how you do it.
2021-11-20 11:39:24 -08:00
Ryan Mulligan 12e5225c9c ci: fix NixOS tests, try macos 2021-11-20 11:37:06 -08:00
Ryan Mulligan c7906a8021 ci: run nix *flake* check 2021-11-20 11:31:38 -08:00
Ryan Mulligan b12f117555 ci: run nix check 2021-11-20 11:30:40 -08:00
Cole Helbling 7bb0b5d7f1 modules/age: add option to disable symlinking
There are some cases where it may be better or even required to have the
secret be a file that is not a symlink. Setting

    age.secrets.some-secret.symlink = false;

will disable the default functionality of symlinking secrets and instead
just forcibly move them to their `path`.
2021-11-15 21:39:32 -08:00
Cole Helbling e538664435 modules/age: /run/secrets -> /run/agenix 2021-11-15 21:39:32 -08:00
Cole Helbling 111754b894 modules/age: remove old secrets generations 2021-11-15 21:39:32 -08:00
Cole Helbling f816a0d5df modules/age: symlink files into place
This follows sops-nix's implementation, where it creates a
`/run/secrets.d` ramfs mountpoint and a "generation" each time
the activation script runs, and then symlinks `/run/secrets` to
`/run/secrets.d/[generation]`.
2021-11-15 21:39:32 -08:00
Ryan Mulligan 53aa91b417
Merge pull request #62 from yaymukund/document-overlay-usage
Document how to install the binary in a `nix-channel` install.
2021-10-16 10:07:08 -07:00
Mukund Lakshman b5cb1a07c0 Document how to install the binary in a `nix-channel` install. 2021-10-16 12:04:16 -04:00
Ryan Mulligan daf1d77398
Merge pull request #59 from ryantm/workaround54
fix: remove workaround for #54
2021-09-17 09:31:09 -07:00
Ryan Mulligan 6d9fdcbd70 fix: remove workaround for #54
https://github.com/NixOS/nixpkgs/pull/137508 should remove the need
for this.
2021-09-16 15:39:38 -07:00
Ryan Mulligan 5c5bc28256
Merge pull request #57 from ryantm/workaround54
fix: workaround for #54
2021-09-10 19:04:24 -07:00
Ryan Mulligan 375a33cd97 fix: workaround for #54 2021-09-10 16:30:05 -07:00
Ryan Mulligan e6752e7b85
Merge pull request #52 from gabysbrain/patch-1
add .nix extensions
2021-08-01 05:56:27 -07:00
Tom Torsney-Weir 1a09f60c3a
add .nix extensions
on my system (21.05.1759.91903ceb294 (Okapi)) I needed to add the .nix extensions on age to get nixos-rebuild to find the module. This seems to be inline with the modules directory structure:
`modules/age/nix`
rather than
`modules/age/default.nix`
but I'm not an expert on nix's file naming conventions
2021-08-01 13:26:50 +02:00
Ryan Mulligan 6e5ca0926e
Merge pull request #49 from ngkz/master
run activation scripts after /run mount
2021-07-30 15:54:13 -07:00
Ryan Mulligan fb00f178b3
Merge pull request #51 from michaeladler/fix/diff-command-not-found
Make 'diff' an explicit dependency
2021-07-22 06:27:35 -07:00
Michael Adler 5c1fbaabc2 Make 'diff' an explicit dependency 2021-07-22 13:58:29 +02:00
Ryan Mulligan 85da8b7366 add meta.description
closes #47
closes #48
2021-07-20 08:50:08 -07:00
Kazutoshi Noguchi 8bad14fe08 run activation scripts after /run mount 2021-07-01 14:13:44 +09:00
Ryan Mulligan e543aa7d68 doc: explain better where SSH host keys come from in tutorial
fixes #17
2021-05-12 20:37:55 -07:00
Ryan Mulligan 20a5c3d256
Merge pull request #44 from ryantm/umask
fix: umask
2021-05-12 20:33:50 -07:00
Ryan Mulligan 400e5208be doc: be more forceful about needing at least 20.09 2021-05-12 20:21:42 -07:00
Ryan Mulligan b69fd62fbb fix: umask
fixes #38
2021-05-12 20:11:17 -07:00
Ryan Mulligan c27b6334a2
Merge pull request #42 from ryantm/flake
fix: stop using flake-utils to fix flake show and flake check
2021-05-10 10:46:18 -07:00
Ryan Mulligan b25c37a869
Merge pull request #40 from ryantm/test
add a NixOS test for setting a user's passwordFile with agenix; and some features/fixes this required
2021-05-10 10:44:18 -07:00
Ryan Mulligan 1ed5f6d3a9 fix: flake show and flake check
remove flake-utils
2021-05-09 15:36:04 -07:00
Ryan Mulligan dd29ebafac Merge remote-tracking branch 'veehaitch/update-flake' into test 2021-05-09 14:27:50 -07:00
Ryan Mulligan 419c6cc281 dev: add integration test 2021-05-09 14:22:48 -07:00
Ryan Mulligan 6aec6889ba feature: use uid 0 and gid 0 as default owner and group (consider them root)
This assumes that the root user is always uid 0 and gid 0, which I
believe is a safe assumption. The reason to add this is because when a
declarative VM (for example, a NixOS test) or image boots the first
time, the installRootOwnedSecrets activation script runs BEFORE the
"users" and "groups" activation scripts, so the user and group for
root is not created. Using uid 0 and gid 0 gets around the root user
not being set up yet.
2021-05-09 14:18:20 -07:00
Ryan Mulligan ecee2c76b9 fix: allow deps of installRootOwnedSecrets activation script to be overridden 2021-05-09 14:17:48 -07:00
Ryan Mulligan c12ac8b6f3
Merge pull request #34 from edrex/patch-1
Extend the tutorial to describe location of decrypted secrets
2021-05-06 06:18:42 -07:00
Ryan Mulligan 204bd95d30 fix: pin more uses of sed 2021-05-04 20:28:24 -07:00
Ryan Mulligan 8e1647070b
Merge pull request #37 from ryantm/specify-binaries
fix: pin down all binaries outside of coreutils
2021-05-04 18:04:10 -07:00
Ryan Mulligan 0b6987f914 fix: pin down all binaries outside of coreutils
The default sed was having trouble with newline splitting on MacOS.
2021-05-04 06:24:31 -07:00
Ryan Mulligan 8652eb6cf3
doc: update readme notice 2021-05-02 18:27:44 -07:00
Vincent Haupert a0e97fd8e7
flake.lock: Update
Flake input changes:

* Updated 'flake-utils': 'github:numtide/flake-utils/3cd06d3c1df6879c9e41cb2c33113df10566c760' -> 'github:numtide/flake-utils/eed214942bcfb3a8cc09eb3b28ca7d7221e44a94'
* Updated 'nixpkgs': 'github:NixOS/nixpkgs/7ff50a7f7b9a701228f870813fe58f01950f870b' -> 'path:/nix/store/z1rf17q0fxj935cmplzys4gg6nxj1as0-source?lastModified=1618628710&narHash=sha256-9xIoU+BrCpjs5nfWcd%2fGlU7XCVdnNKJPffoNTxgGfhs=&rev=7919518f0235106d050c77837df5e338fb94de5d'
2021-04-24 12:32:10 +02:00
Eric Drechsel 838c08e7b2
Update README.md
Co-authored-by: asymmetric <lorenzo@mailbox.org>
2021-04-08 17:03:08 -07:00
Eric Drechsel a64940456c
Update README.md 2021-04-08 11:47:48 -07:00
Eric Drechsel 66374fb29e
Extend the tutorial to describe location of decrypted secrets 2021-04-08 11:43:54 -07:00
Ryan Mulligan f30f0eeb11
Merge pull request #32 from felixscheinost/feature/fix-wrong-import
Fix relative path to `rage.nix`
2021-03-16 10:47:12 -07:00
Felix Scheinost 3f07139990 Fix relative path 2021-03-16 18:31:27 +01:00
Ryan Mulligan 9eb981eeb5
Merge pull request #30 from cole-h/cond-module
modules/age: build local rage if pkgs.rage is older than 0.5.0
2021-03-01 14:08:09 -08:00
Cole Helbling ef7ec993e8
modules/age: build local rage if pkgs.rage is older than 0.5.0 2021-03-01 13:11:02 -08:00
Cole Helbling 9b8f6c01fe
modules/age: nixpkgs-fmt 2021-03-01 13:10:52 -08:00
Ryan Mulligan ed7e69bff3
Merge pull request #28 from cole-h/locale
modules/age: set LANG
2021-02-25 17:25:31 -08:00
Cole Helbling 7ba959742e
modules/age: set LANG
rage has a localization crate as a dependency that whines when LANG
is unset.
2021-02-25 15:16:28 -08:00
Ryan Mulligan a704a85cbd fix Darwin? 2021-02-13 09:46:33 -08:00
Ryan Mulligan ddb81b8bda Merge branch 'rien/master' fix suppory for aarch64 2021-02-08 18:50:16 -08:00
Ryan Mulligan c81f804195
Merge pull request #20 from felixscheinost/master
Need Foundation to build i18n-embed-fl on darwin
2021-02-08 18:46:27 -08:00
Felix Scheinost cd916fad67 Need Foundation to build i18n-embed-fl on darwin 2021-02-04 21:21:23 +01:00
Rien Maertens 017422ed4c
Conditionally build rage if version is below 0.5.0 2021-01-31 22:39:30 +01:00
Rien Maertens a678a8748c
Update rage to latest package definition 2021-01-31 22:39:25 +01:00
Ryan Mulligan 37b1d2aa3f
Merge pull request #12 from blaggacao/da-overlay
add overlay
2020-12-30 13:43:18 -08:00
David Arnold 56b1cb99da
Update overlay.nix
Co-authored-by: Ryan Mulligan <ryan@ryantm.com>
2020-12-30 16:18:38 -05:00
David Arnold f477ca6041
add overlay 2020-12-28 22:39:16 -05:00
Ryan Mulligan 85fd85e318
Merge pull request #10 from AluisioASG/all-non-root-secrets
correctly list non-root secrets
2020-12-21 21:40:20 -08:00
Aluísio Augusto Silva Gonçalves b0a48f587e
correctly list non-root secrets
Secrets that are only partly owned by root (i.e. either user or group
are not 'root') are now accounted for during activation.
2020-12-22 01:34:35 -03:00
Ryan Mulligan 553d1f5caa Merge branch 'flake-nixos-module' 2020-12-19 09:44:43 -08:00
Ryan Mulligan 920acdd8ff add verbose flag 2020-12-19 08:53:44 -08:00
Aluísio Augusto Silva Gonçalves c1cbfe75b0
export module as system-independent flake output
Flake outputs are a mixture of system-dependent and system-independent
sets, and flake-utils doesn't do much to distinguish one from the other.
Because of that, the `age` NixOS module currently has to be acessed as
`agenix.nixosModules.${system}.age`, rather than the documented
`agenix.nixosModules.age`.

To remedy that, (conceptually) split `outputs` in two, let flake-utils
handle the system-dependent half, and merge them to form the actual
outputs.  The names for the two halves were taken from [1].

[1]: https://github.com/NixOS/nix/issues/3843#issuecomment-661720562
2020-12-19 01:53:37 -03:00
Ryan Mulligan 092ba8b166
Merge pull request #7 from ryantm/issue5
use only ~/.ssh/id_rsa and ~/.ssh/id_ed25519 for decryption; friendlier error message when no identity
2020-12-18 20:07:22 -08:00
Ryan Mulligan de625b5298 add friendlier error message in the event of no identity
fixes #6
2020-12-18 20:02:13 -08:00
Ryan Mulligan be7bad2c12 use only ~/.ssh/id_rsa and ~/.ssh/id_ed25519 for decryption
fixes #5
2020-12-18 19:23:47 -08:00
Ryan Mulligan 8af97149b2 Add notice about password-protected ssh keys 2020-12-18 15:41:06 -08:00
Ryan Mulligan d42ba6964b
Merge pull request #3 from bbigras/patch-1
fix typo in README
2020-12-18 11:48:49 -08:00
Bruno Bigras 2f2b526539
fix typo in README 2020-12-18 19:37:23 +00:00
43 changed files with 2462 additions and 300 deletions

35
.github/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name-template: '$RESOLVED_VERSION'
tag-template: '$RESOLVED_VERSION'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Development'
label: 'dev'
- title: '🤖 Dependencies'
label: 'dependencies'
- title: '🔒 Security'
label: 'security'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
template: |
## Changes
$CHANGES

51
.github/workflows/ci.yaml vendored Normal file
View File

@ -0,0 +1,51 @@
name: "CI"
on:
pull_request:
push:
jobs:
tests-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v22
with:
extra_nix_config: |
system-features = nixos-test recursive-nix benchmark big-parallel kvm
extra-experimental-features = recursive-nix nix-command flakes
- run: nix build
- run: nix build .#doc
- run: nix fmt . -- --check
- run: nix flake check
tests-darwin:
runs-on: macos-12
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v24
with:
extra_nix_config: |
system-features = nixos-test recursive-nix benchmark big-parallel kvm
extra-experimental-features = recursive-nix nix-command flakes
- run: nix build
- run: nix build .#doc
- run: nix fmt . -- --check
- run: nix flake check
- name: "Install nix-darwin module"
run: |
# https://github.com/ryantm/agenix/pull/230#issuecomment-1867025385
sudo mv /etc/nix/nix.conf{,.bak}
nix \
--extra-experimental-features 'nix-command flakes' \
build .#checks.x86_64-darwin.integration
./result/activate-user
sudo ./result/activate
- name: "Test nix-darwin module"
run: |
sudo /run/current-system/sw/bin/agenix-integration
- name: "Test home-manager module"
run: |
# Do the job of `home-manager switch` in-line to avoid rate limiting
nix build .#homeConfigurations.integration-darwin.activationPackage
./result/activate
~/agenix-home-integration/bin/agenix-home-integration

41
.github/workflows/doc.yml vendored Normal file
View File

@ -0,0 +1,41 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: [$default-branch]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- uses: cachix/install-nix-action@v20
- run: nix build .#doc && mkdir -p _site/ && cp -r ./result/multi/* _site/
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

View File

@ -0,0 +1,27 @@
name: "Publish tags to FlakeHub"
on:
push:
tags:
- "v?[0-9]+.[0-9]+.[0-9]+*"
workflow_dispatch:
inputs:
tag:
description: "The existing tag to publish to FlakeHub"
type: "string"
required: true
jobs:
flakehub-publish:
runs-on: "ubuntu-latest"
permissions:
id-token: "write"
contents: "read"
steps:
- uses: "actions/checkout@v3"
with:
ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}"
- uses: "DeterminateSystems/nix-installer-action@main"
- uses: "DeterminateSystems/flakehub-push@main"
with:
visibility: "public"
name: "ryantm/agenix"
tag: "${{ inputs.tag }}"

33
.github/workflows/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: Release Drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- main
# pull_request event is required only for autolabeler
pull_request:
# Only following types are handled by the action, but one can default to all as well
types: [opened, reopened, synchronize]
# pull_request_target event is required for autolabeler to support PRs from forks
pull_request_target:
types: [opened, reopened, synchronize]
permissions:
contents: read
jobs:
update_release_draft:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
runs-on: ubuntu-latest
steps:
# Drafts your next Release notes as Pull Requests are merged into "main"
- uses: release-drafter/release-drafter@v5
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/result

526
README.md
View File

@ -1,6 +1,32 @@
# agenix - [age](https://github.com/FiloSottile/age)-encrypted secrets for NixOS
`agenix` is a commandline tool for managing secrets encrypted with your existing SSH keys. This project also includes the NixOS module `age` for adding encrypted secrets into the Nix store and decrypting them.
`agenix` is a small and convenient Nix library for securely managing and deploying secrets using common public-private SSH key pairs:
You can encrypt a secret (password, access-token, etc.) on a source machine using a number of public SSH keys,
and deploy that encrypted secret to any another target machine that has the corresponding private SSH key of one of those public keys.
This project contains two parts:
1. An `agenix` commandline app (CLI) to encrypt secrets into secured `.age` files that can be copied into the Nix store.
2. An `agenix` NixOS module to conveniently
* add those encrypted secrets (`.age` files) into the Nix store so that they can be deployed like any other Nix package using `nixos-rebuild` or similar tools.
* automatically decrypt on a target machine using the private SSH keys on that machine
* automatically mount these decrypted secrets on a well known path like `/run/agenix/...` to be consumed.
## Contents
* [Problem and solution](#problem-and-solution)
* [Features](#features)
* [Installation](#installation)
* [niv](#install-via-niv)
* [nix-channel](#install-via-nix-channel)
* [fetchTarball](#install-via-fetchtarball)
* [flakes](#install-via-flakes)
* [Tutorial](#tutorial)
* [Reference](#reference)
* [`age` module reference](#age-module-reference)
* [agenix CLI reference](#agenix-cli-reference)
* [Community and Support](#community-and-support)
* [Threat model/Warnings](#threat-modelwarnings)
* [Contributing](#contributing)
* [Acknowledgements](#acknowledgements)
## Problem and solution
@ -19,13 +45,16 @@ All files in the Nix store are readable by any system user, so it is not a suita
## Notices
* If you want to manage user's hashed passwords, you must use a version of NixOS with [commit e6b8587](https://github.com/NixOS/nixpkgs/commit/e6b8587b25a19528695c5c270e6ff1c209705c31), so the root-owned secrets can be decrypted before the user activation script runs. Currently only available on `unstable`.
* Password-protected ssh keys: since age does not support ssh-agent, password-protected ssh keys do not work well. For example, if you need to rekey 20 secrets you will have to enter your password 20 times.
## Installation
Choose one of the following methods:
<details>
<summary>
### [niv](https://github.com/nmattia/niv) (Current recommendation)
### Install via [niv](https://github.com/nmattia/niv)
</summary>
First add it to niv:
@ -33,40 +62,78 @@ First add it to niv:
$ niv add ryantm/agenix
```
#### Module
#### Install module via niv
Then add the following to your configuration.nix in the `imports` list:
Then add the following to your `configuration.nix` in the `imports` list:
```nix
{
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age" ];
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age.nix" ];
}
```
### nix-channel
#### Install CLI via niv
As root run:
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage "${(import ./nix/sources.nix).agenix}/pkgs/agenix.nix" {}) ];
}
```
</details>
<details>
<summary>
### Install via nix-channel
</summary>
As root run:
```ShellSession
$ nix-channel --add https://github.com/ryantm/agenix/archive/master.tar.gz agenix
$ nix-channel --update
$ sudo nix-channel --add https://github.com/ryantm/agenix/archive/main.tar.gz agenix
$ sudo nix-channel --update
```
Than add the following to your configuration.nix in the `imports` list:
#### Install module via nix-channel
Then add the following to your `configuration.nix` in the `imports` list:
```nix
{
imports = [ <agenix/modules/age> ];
imports = [ <agenix/modules/age.nix> ];
}
```
### fetchTarball
#### Install CLI via nix-channel
Add the following to your configuration.nix:
To install the `agenix` binary:
```nix
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/master.tar.gz"}/modules/age" ];
environment.systemPackages = [ (pkgs.callPackage <agenix/pkgs/agenix.nix> {}) ];
}
```
</details>
<details>
<summary>
### Install via fetchTarball
</summary>
#### Install module via fetchTarball
Add the following to your configuration.nix:
```nix
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age.nix" ];
}
```
@ -80,22 +147,41 @@ $ nix-channel --update
in [
"${builtins.fetchTarball {
url = "https://github.com/ryantm/agenix/archive/${commit}.tar.gz";
# replace this with an actual hash
sha256 = "0000000000000000000000000000000000000000000000000000";
}}/modules/age"
# update hash from nix build output
sha256 = "";
}}/modules/age.nix"
];
}
```
### Flakes
#### Install CLI via fetchTarball
#### Module
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/pkgs/agenix.nix" {}) ];
}
```
</details>
<details>
<summary>
### Install via Flakes
</summary>
#### Install module via Flakes
```nix
{
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
@ -104,39 +190,69 @@ $ nix-channel --update
system = "x86_64-linux";
modules = [
./configuration.nix
agenix.nixosModules.age
agenix.nixosModules.default
];
};
};
}
```
#### CLI
#### Install CLI via Flakes
You don't need to install it,
You can run the CLI tool ad-hoc without installing it:
```ShellSession
nix run github:ryantm/agenix -- --help
```
but, if you want to (change the system based on your system):
But you can also add it permanently into a [NixOS module](https://wiki.nixos.org/wiki/NixOS_modules)
(replace system "x86_64-linux" with your system):
```nix
{
environment.systemPackages = [ agenix.defaultPackage.x86_64-linux ];
environment.systemPackages = [ agenix.packages.x86_64-linux.default ];
}
```
e.g. inside your `flake.nix` file:
```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 ];
}
];
};
};
}
```
</details>
## Tutorial
1. Make a directory to store secrets and `secrets.nix` file for listing secrets and their public keys:
1. The system you want to deploy secrets to should already exist and
have `sshd` running on it so that it has generated SSH host keys in
`/etc/ssh/`.
2. Make a directory to store secrets and `secrets.nix` file for listing secrets and their public keys:
```ShellSession
$ mkdir secrets
$ cd secerts
$ cd secrets
$ touch secrets.nix
```
2. Add public keys to `secrets.nix` file (hint: use `ssh-keyscan` or GitHub (for example, https://github.com/ryantm.keys)):
This `secrets.nix` file is **not** imported into your NixOS configuration.
It's only used for the `agenix` CLI tool (example below) to know which public keys to use for encryption.
3. Add public keys to your `secrets.nix` file:
```nix
let
user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH";
@ -152,17 +268,310 @@ but, if you want to (change the system based on your system):
"secret2.age".publicKeys = users ++ systems;
}
```
3. Edit secret files (these instructions assume your SSH private key is in ~/.ssh/):
These are the users and systems that will be able to decrypt the `.age` files later with their corresponding private keys.
You can obtain the public keys from
* your local computer usually in `~/.ssh`, e.g. `~/.ssh/id_ed25519.pub`.
* from a running target machine with `ssh-keyscan`:
```ShellSession
$ ssh-keyscan <ip-address>
... ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1
...
```
* from GitHub like https://github.com/ryantm.keys.
4. Create a secret file:
```ShellSession
$ agenix -e secret1.age
```
4. Add secret to a NixOS module config:
It will open a temporary file in the app configured in your $EDITOR environment variable.
When you save that file its content will be encrypted with all the public keys mentioned in the `secrets.nix` file.
5. Add secret to a NixOS module config:
```nix
age.secrets.secret1.file = ../secrets/secret1.age;
{
age.secrets.secret1.file = ../secrets/secret1.age;
}
```
5. NixOS rebuild or use your deployment tool like usual.
When the `age.secrets` attribute set contains a secret, the `agenix` NixOS module will later automatically decrypt and mount that secret under the default path `/run/agenix/secret1`.
Here the `secret1.age` file becomes part of your NixOS deployment, i.e. moves into the Nix store.
## Rekeying
6. Reference the secrets' mount path in your config:
```nix
{
users.users.user1 = {
isNormalUser = true;
passwordFile = config.age.secrets.secret1.path;
};
}
```
You can reference the mount path to the (later) unencrypted secret already in your other configuration.
So `config.age.secrets.secret1.path` will contain the path `/run/agenix/secret1` by default.
7. Use `nixos-rebuild` or [another deployment tool](https://nixos.wiki/wiki/Applications#Deployment") of choice as usual.
The `secret1.age` file will be copied over to the target machine like any other Nix package.
Then it will be decrypted and mounted as described before.
8. Edit secret files:
```ShellSession
$ agenix -e secret1.age
```
It assumes your SSH private key is in `~/.ssh/`.
In order to decrypt and open a `.age` file for editing you need the private key of one of the public keys
it was encrypted with. You can pass the private key you want to use explicitly with `-i`, e.g.
```ShellSession
$ agenix -e secret1.age -i ~/.ssh/id_ed25519
```
## Reference
### `age` module reference
#### `age.secrets`
`age.secrets` attrset of secrets. You always need to use this
configuration option. Defaults to `{}`.
#### `age.secrets.<name>.file`
`age.secrets.<name>.file` is the path to the encrypted `.age` for this
secret. This is the only required secret option.
Example:
```nix
{
age.secrets.monitrc.file = ../secrets/monitrc.age;
}
```
#### `age.secrets.<name>.path`
`age.secrets.<name>.path` is the path where the secret is decrypted
to. Defaults to `/run/agenix/<name>` (`config.age.secretsDir/<name>`).
Example defining a different path:
```nix
{
age.secrets.monitrc = {
file = ../secrets/monitrc.age;
path = "/etc/monitrc";
};
}
```
For many services, you do not need to set this. Instead, refer to the
decryption path in your configuration with
`config.age.secrets.<name>.path`.
Example referring to path:
```nix
{
users.users.ryantm = {
isNormalUser = true;
passwordFile = config.age.secrets.passwordfile-ryantm.path;
};
}
```
##### builtins.readFile anti-pattern
```nix
{
# Do not do this!
config.password = builtins.readFile config.age.secrets.secret1.path;
}
```
This can cause the cleartext to be placed into the world-readable Nix
store. Instead, have your services read the cleartext path at runtime.
#### `age.secrets.<name>.mode`
`age.secrets.<name>.mode` is permissions mode of the decrypted secret
in a format understood by chmod. Usually, you only need to use this in
combination with `age.secrets.<name>.owner` and
`age.secrets.<name>.group`
Example:
```nix
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
```
#### `age.secrets.<name>.owner`
`age.secrets.<name>.owner` is the username of the decrypted file's
owner. Usually, you only need to use this in combination with
`age.secrets.<name>.mode` and `age.secrets.<name>.group`
Example:
```nix
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
```
#### `age.secrets.<name>.group`
`age.secrets.<name>.group` is the name of the decrypted file's
group. Usually, you only need to use this in combination with
`age.secrets.<name>.owner` and `age.secrets.<name>.mode`
Example:
```nix
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
```
#### `age.secrets.<name>.symlink`
`age.secrets.<name>.symlink` is a boolean. If true (the default),
secrets are symlinked to `age.secrets.<name>.path`. If false, secrets
are copied to `age.secrets.<name>.path`. Usually, you want to keep
this as true, because it secure cleanup of secrets no longer
used. (The symlink will still be there, but it will be broken.) If
false, you are responsible for cleaning up your own secrets after you
stop using them.
Some programs do not like following symlinks (for example Java
programs like Elasticsearch).
Example:
```nix
{
age.secrets."elasticsearch.conf" = {
file = ../secrets/elasticsearch.conf.age;
symlink = false;
};
}
```
#### `age.secrets.<name>.name`
`age.secrets.<name>.name` is the string of the name of the file after
it is decrypted. Defaults to the `<name>` in the attrpath, but can be
set separately if you want the file name to be different from the
attribute name part.
Example of a secret with a name different from its attrpath:
```nix
{
age.secrets.monit = {
name = "monitrc";
file = ../secrets/monitrc.age;
};
}
```
#### `age.ageBin`
`age.ageBin` the string of the path to the `age` binary. Usually, you
don't need to change this. Defaults to `age/bin/age`.
Overriding `age.ageBin` example:
```nix
{pkgs, ...}:{
age.ageBin = "${pkgs.age}/bin/age";
}
```
#### `age.identityPaths`
`age.identityPaths` is a list of paths to recipient keys to try to use to
decrypt the secrets. By default, it is the `rsa` and `ed25519` keys in
`config.services.openssh.hostKeys`, and on NixOS you usually don't need to
change this. The list items should be strings (`"/path/to/id_rsa"`), not
nix paths (`../path/to/id_rsa`), as the latter would copy your private key to
the nix store, which is the exact situation `agenix` is designed to avoid. At
least one of the file paths must be present at runtime and able to decrypt the
secret in question. Overriding `age.identityPaths` example:
```nix
{
age.identityPaths = [ "/var/lib/persistent/ssh_host_ed25519_key" ];
}
```
#### `age.secretsDir`
`age.secretsDir` is the directory where secrets are symlinked to by
default. Usually, you don't need to change this. Defaults to
`/run/agenix`.
Overriding `age.secretsDir` example:
```nix
{
age.secretsDir = "/run/keys";
}
```
#### `age.secretsMountPoint`
`age.secretsMountPoint` is the directory where the secret generations
are created before they are symlinked. Usually, you don't need to
change this. Defaults to `/run/agenix.d`.
Overriding `age.secretsMountPoint` example:
```nix
{
age.secretsMountPoint = "/run/secret-generations";
}
```
### agenix CLI reference
```
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'
```
#### Rekeying
If you change the public keys in `secrets.nix`, you should rekey your
secrets:
@ -176,9 +585,27 @@ randomness in `age`'s encryption algorithms, the files always change
when rekeyed, even if the identities do not. (This eventually could be
improved upon by reading the identities from the age file.)
#### Overriding age binary
The agenix CLI uses `age` by default as its age implemenation, you
can use the `rage` implementation with Flakes like this:
```nix
{pkgs,agenix,...}:{
environment.systemPackages = [
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
];
}
```
## Community and Support
Support and development discussion is available here on GitHub and
also through [Matrix](https://matrix.to/#/#agenix:nixos.org).
## Threat model/Warnings
This project has not be audited by a security professional.
This project has not been audited by a security professional.
People unfamiliar with `age` might be surprised that secrets are not
authenticated. This means that every attacker that has write access to
@ -191,6 +618,35 @@ authentication code (MAC) like other implementations like GPG or
[sops](https://github.com/Mic92/sops-nix) have, however this was left
out for simplicity in `age`.
## Contributing
* The main branch is protected against direct pushes
* All changes must go through GitHub PR review and get at least one approval
* PR titles and commit messages should be prefixed with at least one of these categories:
* contrib - things that make the project development better
* doc - documentation
* feature - new features
* fix - bug fixes
* Please update or make integration tests for new features
* Use `nix fmt` to format nix code
### Tests
You can run the tests with
```ShellSession
nix flake check
```
You can run the integration tests in interactive mode like this:
```ShellSession
nix run .#checks.x86_64-linux.integration.driverInteractive
```
After it starts, enter `run_tests()` to run the tests.
## Acknowledgements
This project is based off of [sops-nix](https://github.com/Mic92/sops-nix) created Mic92. Thank you to Mic92 for inspiration and advice.

8
contrib/_incr_version Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
grep -q "$1" pkgs/agenix.nix || (echo "Couldn't find version $1 in pkgs/agenix.nix" && exit 1)
sed -i "s/$1/$2/g" pkgs/agenix.nix
git add pkgs/agenix.nix
git commit -m "version $2"
exit 0

67
contrib/release Executable file
View File

@ -0,0 +1,67 @@
#!/usr/bin/env nix-shell
#! nix-shell -p python3 -i python
# based off of https://git.sr.ht/~sircmpwn/dotfiles/tree/master/item/bin/semver
import os
import subprocess
import sys
import tempfile
if subprocess.run(["git", "branch", "--show-current"], stdout=subprocess.PIPE
).stdout.decode().strip() != "main":
print("WARNING! Not on the main branch.")
subprocess.run(["git", "pull", "--rebase"])
p = subprocess.run(["git", "describe", "--abbrev=0"], stdout=subprocess.PIPE)
describe = p.stdout.decode().strip()
old_version = describe.split("-")[0].split(".")
if len(old_version) == 2:
[major, minor] = old_version
[major, minor] = map(int, [major, minor])
patch = 0
else:
[major, minor, patch] = old_version
[major, minor, patch] = map(int, [major, minor, patch])
p = subprocess.run(["git", "shortlog", "--no-merges", f"{describe}..HEAD"],
stdout=subprocess.PIPE)
shortlog = p.stdout.decode()
new_version = None
if sys.argv[1] == "patch":
patch += 1
elif sys.argv[1] == "minor":
minor += 1
patch = 0
elif sys.argv[1] == "major":
major += 1
minor = patch = 0
else:
new_version = sys.argv[1]
if new_version is None:
if len(old_version) == 2 and patch == 0:
new_version = f"{major}.{minor}"
else:
new_version = f"{major}.{minor}.{patch}"
p = None
if os.path.exists("contrib/_incr_version"):
p = subprocess.run(["contrib/_incr_version", describe, new_version])
else:
print("Warning: no _incr_version script. " +
"Does this project have any specific release requirements?")
if p and p.returncode != 0:
print("Error: _incr_version returned nonzero exit code")
sys.exit(1)
with tempfile.NamedTemporaryFile() as f:
basename = os.path.basename(os.getcwd())
f.write(f"{basename} {new_version}\n\n".encode())
f.write(shortlog.encode())
f.flush()
subprocess.run(["git", "tag", "-e", "-F", f.name, "-a", new_version])
print(new_version)

View File

@ -1,4 +1,3 @@
{ pkgs ? import <nixpkgs> {} }:
{
agenix = pkgs.callPackage ./pkgs/agenix.nix {};
{pkgs ? import <nixpkgs> {}}: {
agenix = pkgs.callPackage ./pkgs/agenix.nix {};
}

3
doc/acknowledgements.md Normal file
View File

@ -0,0 +1,3 @@
# Acknowledgements {#acknowledgements}
This project is based off of [sops-nix](https://github.com/Mic92/sops-nix) created Mic92. Thank you to Mic92 for inspiration and advice.

View File

@ -0,0 +1,4 @@
# Community and Support {#community-and-support}
Support and development discussion is available here on GitHub and
also through [Matrix](https://matrix.to/#/#agenix:nixos.org).

28
doc/contributing.md Normal file
View File

@ -0,0 +1,28 @@
# Contributing {#contributing}
* The main branch is protected against direct pushes
* All changes must go through GitHub PR review and get at least one approval
* PR titles and commit messages should be prefixed with at least one of these categories:
* contrib - things that make the project development better
* doc - documentation
* feature - new features
* fix - bug fixes
* Please update or make integration tests for new features
* Use `nix fmt` to format nix code
## Tests
You can run the tests with
```ShellSession
nix flake check
```
You can run the integration tests in interactive mode like this:
```ShellSession
nix run .#checks.x86_64-linux.integration.driverInteractive
```
After it starts, enter `run_tests()` to run the tests.

8
doc/features.md Normal file
View File

@ -0,0 +1,8 @@
# Features {#features}
* Secrets are encrypted with SSH keys
* system public keys via `ssh-keyscan`
* can use public keys available on GitHub for users (for example, https://github.com/ryantm.keys)
* No GPG
* Very little code, so it should be easy for you to audit
* Encrypted secrets are stored in the Nix store, so a separate distribution mechanism is not necessary

View File

@ -0,0 +1,38 @@
# Install via fetchTarball {#install-via-fetchtarball}
#### Install module via fetchTarball
Add the following to your configuration.nix:
```nix
{
imports = [ "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age.nix" ];
}
```
or with pinning:
```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"
];
}
```
#### Install CLI via fetchTarball
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/pkgs/agenix.nix" {}) ];
}
```

39
doc/install-via-flakes.md Normal file
View File

@ -0,0 +1,39 @@
# Install via Flakes {#install-via-flakes}
## Install module via Flakes
```nix
{
inputs.agenix.url = "github:ryantm/agenix";
# optional, not necessary for the module
#inputs.agenix.inputs.nixpkgs.follows = "nixpkgs";
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
];
};
};
}
```
## Install CLI via Flakes
You don't need to install it,
```ShellSession
nix run github:ryantm/agenix -- --help
```
but, if you want to (change the system based on your system):
```nix
{
environment.systemPackages = [ agenix.packages.x86_64-linux.default ];
}
```

27
doc/install-via-niv.md Normal file
View File

@ -0,0 +1,27 @@
# Install via [niv](https://github.com/nmattia/niv) {#install-via-niv}
First add it to niv:
```ShellSession
$ niv add ryantm/agenix
```
## Install module via niv
Then add the following to your `configuration.nix` in the `imports` list:
```nix
{
imports = [ "${(import ./nix/sources.nix).agenix}/modules/age.nix" ];
}
```
## Install CLI via niv
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage "${(import ./nix/sources.nix).agenix}/pkgs/agenix.nix" {}) ];
}
```

View File

@ -0,0 +1,28 @@
# Install via nix-channel {#install-via-nix-channel}
As root run:
```ShellSession
$ sudo nix-channel --add https://github.com/ryantm/agenix/archive/main.tar.gz agenix
$ sudo nix-channel --update
```
## Install module via nix-channel
Then add the following to your `configuration.nix` in the `imports` list:
```nix
{
imports = [ <agenix/modules/age.nix> ];
}
```
## Install CLI via nix-channel
To install the `agenix` binary:
```nix
{
environment.systemPackages = [ (pkgs.callPackage <agenix/pkgs/agenix.nix> {}) ];
}
```

3
doc/introduction.md Normal file
View File

@ -0,0 +1,3 @@
# agenix - [age](https://github.com/FiloSottile/age)-encrypted secrets for NixOS {#introduction}
`agenix` is a commandline tool for managing secrets encrypted with your existing SSH keys. This project also includes the NixOS module `age` for adding encrypted secrets into the Nix store and decrypting them.

3
doc/notices.md Normal file
View File

@ -0,0 +1,3 @@
# Notices {#notices}
* Password-protected ssh keys: since age does not support ssh-agent, password-protected ssh keys do not work well. For example, if you need to rekey 20 secrets you will have to enter your password 20 times.

View File

@ -0,0 +1,12 @@
# Overriding age binary {#overriding-age-binary}
The agenix CLI uses `age` by default as its age implemenation, you
can use the `rage` implementation with Flakes like this:
```nix
{pkgs,agenix,...}:{
environment.systemPackages = [
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
];
}
```

View File

@ -0,0 +1,5 @@
# Problem and solution {#problem-and-solution}
All files in the Nix store are readable by any system user, so it is not a suitable place for including cleartext secrets. Many existing tools (like NixOps deployment.keys) deploy secrets separately from `nixos-rebuild`, making deployment, caching, and auditing more difficult. Out-of-band secret management is also less reproducible.
`agenix` solves these issues by using your pre-existing SSH key infrastructure and `age` to encrypt secrets into the Nix store. Secrets are decrypted using an SSH host private key during NixOS system activation.

250
doc/reference.md Normal file
View File

@ -0,0 +1,250 @@
# Reference {#reference}
## `age` module reference {#age-module-reference}
### `age.secrets`
`age.secrets` attrset of secrets. You always need to use this
configuration option. Defaults to `{}`.
### `age.secrets.<name>.file`
`age.secrets.<name>.file` is the path to the encrypted `.age` for this
secret. This is the only required secret option.
Example:
```nix
{
age.secrets.monitrc.file = ../secrets/monitrc.age;
}
```
### `age.secrets.<name>.path`
`age.secrets.<name>.path` is the path where the secret is decrypted
to. Defaults to `/run/agenix/<name>` (`config.age.secretsDir/<name>`).
Example defining a different path:
```nix
{
age.secrets.monitrc = {
file = ../secrets/monitrc.age;
path = "/etc/monitrc";
};
}
```
For many services, you do not need to set this. Instead, refer to the
decryption path in your configuration with
`config.age.secrets.<name>.path`.
Example referring to path:
```nix
{
users.users.ryantm = {
isNormalUser = true;
passwordFile = config.age.secrets.passwordfile-ryantm.path;
};
}
```
#### builtins.readFile anti-pattern
```nix
{
# Do not do this!
config.password = builtins.readFile config.age.secrets.secret1.path;
}
```
This can cause the cleartext to be placed into the world-readable Nix
store. Instead, have your services read the cleartext path at runtime.
### `age.secrets.<name>.mode`
`age.secrets.<name>.mode` is permissions mode of the decrypted secret
in a format understood by chmod. Usually, you only need to use this in
combination with `age.secrets.<name>.owner` and
`age.secrets.<name>.group`
Example:
```nix
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
```
### `age.secrets.<name>.owner`
`age.secrets.<name>.owner` is the username of the decrypted file's
owner. Usually, you only need to use this in combination with
`age.secrets.<name>.mode` and `age.secrets.<name>.group`
Example:
```nix
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
```
### `age.secrets.<name>.group`
`age.secrets.<name>.group` is the name of the decrypted file's
group. Usually, you only need to use this in combination with
`age.secrets.<name>.owner` and `age.secrets.<name>.mode`
Example:
```nix
{
age.secrets.nginx-htpasswd = {
file = ../secrets/nginx.htpasswd.age;
mode = "770";
owner = "nginx";
group = "nginx";
};
}
```
### `age.secrets.<name>.symlink`
`age.secrets.<name>.symlink` is a boolean. If true (the default),
secrets are symlinked to `age.secrets.<name>.path`. If false, secerts
are copied to `age.secrets.<name>.path`. Usually, you want to keep
this as true, because it secure cleanup of secrets no longer
used. (The symlink will still be there, but it will be broken.) If
false, you are responsible for cleaning up your own secrets after you
stop using them.
Some programs do not like following symlinks (for example Java
programs like Elasticsearch).
Example:
```nix
{
age.secrets."elasticsearch.conf" = {
file = ../secrets/elasticsearch.conf.age;
symlink = false;
};
}
```
### `age.secrets.<name>.name`
`age.secrets.<name>.name` is the string of the name of the file after
it is decrypted. Defaults to the `<name>` in the attrpath, but can be
set separately if you want the file name to be different from the
attribute name part.
Example of a secret with a name different from its attrpath:
```nix
{
age.secrets.monit = {
name = "monitrc";
file = ../secrets/monitrc.age;
};
}
```
### `age.ageBin`
`age.ageBin` the string of the path to the `age` binary. Usually, you
don't need to change this. Defaults to `age/bin/age`.
Overriding `age.ageBin` example:
```nix
{pkgs, ...}:{
age.ageBin = "${pkgs.age}/bin/age";
}
```
### `age.identityPaths`
`age.identityPaths` is a list of paths to recipient keys to try to use to
decrypt the secrets. By default, it is the `rsa` and `ed25519` keys in
`config.services.openssh.hostKeys`, and on NixOS you usually don't need to
change this. The list items should be strings (`"/path/to/id_rsa"`), not
nix paths (`../path/to/id_rsa`), as the latter would copy your private key to
the nix store, which is the exact situation `agenix` is designed to avoid. At
least one of the file paths must be present at runtime and able to decrypt the
secret in question. Overriding `age.identityPaths` example:
```nix
{
age.identityPaths = [ "/var/lib/persistent/ssh_host_ed25519_key" ];
}
```
### `age.secretsDir`
`age.secretsDir` is the directory where secrets are symlinked to by
default.Usually, you don't need to change this. Defaults to
`/run/agenix`.
Overriding `age.secretsDir` example:
```nix
{
age.secretsDir = "/run/keys";
}
```
### `age.secretsMountPoint`
`age.secretsMountPoint` is the directory where the secret generations
are created before they are symlinked. Usually, you don't need to
change this. Defaults to `/run/agenix.d`.
Overriding `age.secretsMountPoint` example:
```nix
{
age.secretsMountPoint = "/run/secret-generations";
}
```
## agenix CLI reference {#agenix-cli-reference}
```
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'

13
doc/rekeying.md Normal file
View File

@ -0,0 +1,13 @@
# Rekeying {#rekeying}
If you change the public keys in `secrets.nix`, you should rekey your
secrets:
```ShellSession
$ agenix --rekey
```
To rekey a secret, you have to be able to decrypt it. Because of
randomness in `age`'s encryption algorithms, the files always change
when rekeyed, even if the identities do not. (This eventually could be
improved upon by reading the identities from the age file.)

View File

@ -0,0 +1,14 @@
# Threat model/Warnings {#threat-model-warnings}
This project has not been audited by a security professional.
People unfamiliar with `age` might be surprised that secrets are not
authenticated. This means that every attacker that has write access to
the secret files can modify secrets because public keys are exposed.
This seems like not a problem on the first glance because changing the
configuration itself could expose secrets easily. However, reviewing
configuration changes is easier than reviewing random secrets (for
example, 4096-bit rsa keys). This would be solved by having a message
authentication code (MAC) like other implementations like GPG or
[sops](https://github.com/Mic92/sops-nix) have, however this was left
out for simplicity in `age`.

18
doc/toc.md Normal file
View File

@ -0,0 +1,18 @@
# agenix
* [Introduction](#introduction)
* [Problem and solution](#problem-and-solution)
* [Features](#features)
* Installation
* [flakes](#install-via-flakes)
* [niv](#install-via-niv)
* [fetchTarball](#install-via-fetchtarball)
* [nix-channel](#install-via-nix-channel)
* [Tutorial](#tutorial)
* [Reference](#reference)
* [`age` module reference](#age-module-reference)
* [agenix CLI reference](#agenix-cli-reference)
* [Community and Support](#community-and-support)
* [Threat model/Warnings](#threat-model-warnings)
* [Contributing](#contributing)
* [Acknowledgements](#acknowledgements)

51
doc/tutorial.md Normal file
View File

@ -0,0 +1,51 @@
# Tutorial {#tutorial}
1. The system you want to deploy secrets to should already exist and
have `sshd` running on it so that it has generated SSH host keys in
`/etc/ssh/`.
2. Make a directory to store secrets and `secrets.nix` file for listing secrets and their public keys (This file is **not** imported into your NixOS configuration. It is only used for the `agenix` CLI.):
```ShellSession
$ mkdir secrets
$ cd secrets
$ touch secrets.nix
```
3. Add public keys to `secrets.nix` file (hint: use `ssh-keyscan` or GitHub (for example, https://github.com/ryantm.keys)):
```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;
}
```
4. Edit secret files (these instructions assume your SSH private key is in ~/.ssh/):
```ShellSession
$ agenix -e secret1.age
```
5. Add secret to a NixOS module config:
```nix
{
age.secrets.secret1.file = ../secrets/secret1.age;
}
```
6. Use the secret in your config:
```nix
{
users.users.user1 = {
isNormalUser = true;
passwordFile = config.age.secrets.secret1.path;
};
}
```
7. NixOS rebuild or use your deployment tool like usual.
The secret will be decrypted to the value of `config.age.secrets.secret1.path` (`/run/agenix/secret1` by default).

View File

@ -0,0 +1,9 @@
age-encryption.org/v1
-> ssh-ed25519 KLPP8w s1DYZRlZuSsyhmZCF1lFB+E9vB8bZ/+ZhBRlx8nprwE
nmYVCsVBrX2CFXXPU+D+bbkkIe/foofp+xoUrg9DHZw
-> ssh-ed25519 V3XmEA Pwv3oCwcY0DX8rY48UNfsj9RumWsn4dbgorYHCwObgI
FKxRYkL3JHtJxUwymWDF0rAtJ33BivDI6IfPsfumM90
-> V'v(/u$-grease em/Vgf 2qDuk
7I3iiQLPGi1COML9u/JeYkr7EqbSLoU
--- 57WJRigUGtmcObrssS3s4PvmR8wgh1AOC/ijJn1s3xI
<EFBFBD>'K©Æ·Y&7GÆOÝòFj±kÆXç«BnuJöê:9Ê(ÙÏX¬#¼AíÄÞÃÚ§j,ê_ÈþÝ?ÝZ“¥vœ¹V96]oks~%£c Îe^CÅ%JQ5€<H¢z}îCý,°pŒ¿*!W§§ÈA±º­Ò…dC¼K)¿¢-žy

View File

@ -1,8 +1,8 @@
let
user1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH";
system1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPJDyIr/FSz1cJdcoW69R+NrWzwGK/+3gJpqD1t8L2zE";
in
{
"secret1.age".publicKeys = [ user1 system1 ];
"secret2.age".publicKeys = [ user1 ];
in {
"secret1.age".publicKeys = [user1 system1];
"secret2.age".publicKeys = [user1];
"passwordfile-user1.age".publicKeys = [user1 system1];
}

View File

@ -1,38 +1,83 @@
{
"nodes": {
"flake-utils": {
"darwin": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1600209923,
"narHash": "sha256-zoOWauTliFEjI++esk6Jzk7QO5EKpddWXQm9yQK24iM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3cd06d3c1df6879c9e41cb2c33113df10566c760",
"lastModified": 1700795494,
"narHash": "sha256-gzGLZSiOhf155FW7262kdHo2YDeugp3VuIFb4/GGng0=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "4b9b83d5a92e8c1fbfd8eb27eda375908c11ec4d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "lnl7",
"ref": "master",
"repo": "nix-darwin",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1703113217,
"narHash": "sha256-7ulcXOk63TIT2lVDSExj7XzFx09LpdSAPtvgtM7yQPE=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "3bfaacf46133c037bb356193bd2f1765d9dc82c1",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1599148892,
"narHash": "sha256-V76c6DlI0ZZffvbBpxGlpVSpXxZ14QpFHwAvEEujIsY=",
"lastModified": 1703013332,
"narHash": "sha256-+tFNwMvlXLbJZXiMHqYq77z/RfmpfpiI3yjL6o/Zo9M=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7ff50a7f7b9a701228f870813fe58f01950f870b",
"rev": "54aac082a4d9bb5bbc5c4e899603abfb76a3f6d6",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"darwin": "darwin",
"home-manager": "home-manager",
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},

View File

@ -1,13 +1,89 @@
{
description = "Secret management with age";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
darwin = {
url = "github:lnl7/nix-darwin/master";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
systems.url = "github:nix-systems/default";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
{
nixosModules.age = import ./modules/age.nix;
packages = nixpkgs.legacyPackages.${system}.callPackage ./default.nix {};
defaultPackage = self.packages.${system}.agenix;
});
outputs = {
self,
nixpkgs,
darwin,
home-manager,
systems,
}: let
eachSystem = nixpkgs.lib.genAttrs (import systems);
in {
nixosModules.age = import ./modules/age.nix;
nixosModules.default = self.nixosModules.age;
darwinModules.age = import ./modules/age.nix;
darwinModules.default = self.darwinModules.age;
homeManagerModules.age = import ./modules/age-home.nix;
homeManagerModules.default = self.homeManagerModules.age;
overlays.default = import ./overlay.nix;
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
packages = eachSystem (system: {
agenix = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {};
doc = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/doc.nix {inherit self;};
default = self.packages.${system}.agenix;
});
checks =
nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: {
integration =
(darwin.lib.darwinSystem {
inherit system;
modules = [
./test/integration_darwin.nix
# Allow new-style nix commands in CI
{nix.extraOptions = "experimental-features = nix-command flakes";}
home-manager.darwinModules.home-manager
{
home-manager = {
verbose = true;
useGlobalPkgs = true;
useUserPackages = true;
backupFileExtension = "hmbak";
users.runner = ./test/integration_hm_darwin.nix;
};
}
];
})
.system;
})
// {
x86_64-linux.integration = import ./test/integration.nix {
inherit nixpkgs home-manager;
pkgs = nixpkgs.legacyPackages.x86_64-linux;
system = "x86_64-linux";
};
};
darwinConfigurations.integration-x86_64.system = self.checks.x86_64-darwin.integration;
darwinConfigurations.integration-aarch64.system = self.checks.aarch64-darwin.integration;
# Work-around for https://github.com/nix-community/home-manager/issues/3075
legacyPackages = nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: {
homeConfigurations.integration-darwin = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.${system};
modules = [./test/integration_hm_darwin.nix];
};
});
};
}

237
modules/age-home.nix Normal file
View File

@ -0,0 +1,237 @@
{
config,
options,
lib,
pkgs,
...
}:
with lib; let
cfg = config.age;
ageBin = lib.getExe config.age.package;
newGeneration = ''
_agenix_generation="$(basename "$(readlink "${cfg.secretsDir}")" || echo 0)"
(( ++_agenix_generation ))
echo "[agenix] creating new generation in ${cfg.secretsMountPoint}/$_agenix_generation"
mkdir -p "${cfg.secretsMountPoint}"
chmod 0751 "${cfg.secretsMountPoint}"
mkdir -p "${cfg.secretsMountPoint}/$_agenix_generation"
chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation"
'';
setTruePath = secretType: ''
${
if secretType.symlink
then ''
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
''
else ''
_truePath="${secretType.path}"
''
}
'';
installSecret = secretType: ''
${setTruePath secretType}
echo "decrypting '${secretType.file}' to '$_truePath'..."
TMP_FILE="$_truePath.tmp"
IDENTITIES=()
# shellcheck disable=2043
for identity in ${toString cfg.identityPaths}; do
test -r "$identity" || continue
IDENTITIES+=(-i)
IDENTITIES+=("$identity")
done
test "''${#IDENTITIES[@]}" -eq 0 && echo "[agenix] WARNING: no readable identities found!"
mkdir -p "$(dirname "$_truePath")"
# shellcheck disable=SC2193,SC2050
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && mkdir -p "$(dirname "${secretType.path}")"
(
umask u=r,g=,o=
test -f "${secretType.file}" || echo '[agenix] WARNING: encrypted file ${secretType.file} does not exist!'
test -d "$(dirname "$TMP_FILE")" || echo "[agenix] WARNING: $(dirname "$TMP_FILE") does not exist!"
LANG=${config.i18n.defaultLocale or "C"} ${ageBin} --decrypt "''${IDENTITIES[@]}" -o "$TMP_FILE" "${secretType.file}"
)
chmod ${secretType.mode} "$TMP_FILE"
mv -f "$TMP_FILE" "$_truePath"
${optionalString secretType.symlink ''
# shellcheck disable=SC2193,SC2050
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfn "${cfg.secretsDir}/${secretType.name}" "${secretType.path}"
''}
'';
testIdentities =
map
(path: ''
test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!'
'')
cfg.identityPaths;
cleanupAndLink = ''
_agenix_generation="$(basename "$(readlink "${cfg.secretsDir}")" || echo 0)"
(( ++_agenix_generation ))
echo "[agenix] symlinking new secrets to ${cfg.secretsDir} (generation $_agenix_generation)..."
ln -sfn "${cfg.secretsMountPoint}/$_agenix_generation" "${cfg.secretsDir}"
(( _agenix_generation > 1 )) && {
echo "[agenix] removing old secrets (generation $(( _agenix_generation - 1 )))..."
rm -rf "${cfg.secretsMountPoint}/$(( _agenix_generation - 1 ))"
}
'';
installSecrets = builtins.concatStringsSep "\n" (
["echo '[agenix] decrypting secrets...'"]
++ testIdentities
++ (map installSecret (builtins.attrValues cfg.secrets))
++ [cleanupAndLink]
);
secretType = types.submodule ({
config,
name,
...
}: {
options = {
name = mkOption {
type = types.str;
default = name;
description = ''
Name of the file used in ''${cfg.secretsDir}
'';
};
file = mkOption {
type = types.path;
description = ''
Age file the secret is loaded from.
'';
};
path = mkOption {
type = types.str;
default = "${cfg.secretsDir}/${config.name}";
description = ''
Path where the decrypted secret is installed.
'';
};
mode = mkOption {
type = types.str;
default = "0400";
description = ''
Permissions mode of the decrypted secret in a format understood by chmod.
'';
};
symlink = mkEnableOption "symlinking secrets to their destination" // {default = true;};
};
});
mountingScript = let
app = pkgs.writeShellApplication {
name = "agenix-home-manager-mount-secrets";
runtimeInputs = with pkgs; [coreutils];
text = ''
${newGeneration}
${installSecrets}
exit 0
'';
};
in
lib.getExe app;
userDirectory = dir: let
inherit (pkgs.stdenv.hostPlatform) isDarwin;
baseDir =
if isDarwin
then "$(getconf DARWIN_USER_TEMP_DIR)"
else "$XDG_RUNTIME_DIR";
in "${baseDir}/${dir}";
userDirectoryDescription = dir:
literalExpression ''
"$XDG_RUNTIME_DIR"/${dir} on linux or "$(getconf DARWIN_USER_TEMP_DIR)"/${dir} on darwin.
'';
in {
options.age = {
package = mkPackageOption pkgs "age" {};
secrets = mkOption {
type = types.attrsOf secretType;
default = {};
description = ''
Attrset of secrets.
'';
};
identityPaths = mkOption {
type = types.listOf types.path;
default = [
"${config.home.homeDirectory}/.ssh/id_ed25519"
"${config.home.homeDirectory}/.ssh/id_rsa"
];
defaultText = literalExpression ''
[
"''${config.home.homeDirectory}/.ssh/id_ed25519"
"''${config.home.homeDirectory}/.ssh/id_rsa"
]
'';
description = ''
Path to SSH keys to be used as identities in age decryption.
'';
};
secretsDir = mkOption {
type = types.str;
default = userDirectory "agenix";
defaultText = userDirectoryDescription "agenix";
description = ''
Folder where secrets are symlinked to
'';
};
secretsMountPoint = mkOption {
default = userDirectory "agenix.d";
defaultText = userDirectoryDescription "agenix.d";
description = ''
Where secrets are created before they are symlinked to ''${cfg.secretsDir}
'';
};
};
config = mkIf (cfg.secrets != {}) {
assertions = [
{
assertion = cfg.identityPaths != [];
message = "age.identityPaths must be set.";
}
];
systemd.user.services.agenix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
Unit = {
Description = "agenix activation";
};
Service = {
Type = "oneshot";
ExecStart = mountingScript;
};
Install.WantedBy = ["default.target"];
};
launchd.agents.activate-agenix = {
enable = true;
config = {
ProgramArguments = [mountingScript];
KeepAlive = {
Crashed = false;
SuccessfulExit = false;
};
RunAtLoad = true;
ProcessType = "Background";
StandardOutPath = "${config.home.homeDirectory}/Library/Logs/agenix/stdout";
StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/agenix/stderr";
};
};
};
}

View File

@ -1,38 +1,142 @@
{ config, lib, pkgs, ... }:
with lib;
let
{
config,
options,
lib,
pkgs,
...
}:
with lib; let
cfg = config.age;
rage = pkgs.callPackage ../pkgs/rage.nix {};
ageBin = "${rage}/bin/rage";
isDarwin = lib.attrsets.hasAttrByPath ["environment" "darwinConfig"] options;
ageBin = config.age.ageBin;
users = config.users.users;
identities = builtins.concatStringsSep " " (map (path: "-i ${path}") cfg.sshKeyPaths);
installSecret = secretType: ''
echo "decrypting ${secretType.file} to ${secretType.path}..."
TMP_FILE="${secretType.path}.tmp"
mkdir -p $(dirname ${secretType.path})
(umask 0400; ${ageBin} --decrypt ${identities} -o "$TMP_FILE" "${secretType.file}")
chmod ${secretType.mode} "$TMP_FILE"
chown ${secretType.owner}:${secretType.group} "$TMP_FILE"
mv -f "$TMP_FILE" '${secretType.path}'
mountCommand =
if isDarwin
then ''
if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then
num_sectors=1048576
dev=$(hdiutil attach -nomount ram://"$num_sectors" | sed 's/[[:space:]]*$//')
newfs_hfs -v agenix "$dev"
mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}"
fi
''
else ''
grep -q "${cfg.secretsMountPoint} ramfs" /proc/mounts ||
mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0751
'';
newGeneration = ''
_agenix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)"
(( ++_agenix_generation ))
echo "[agenix] creating new generation in ${cfg.secretsMountPoint}/$_agenix_generation"
mkdir -p "${cfg.secretsMountPoint}"
chmod 0751 "${cfg.secretsMountPoint}"
${mountCommand}
mkdir -p "${cfg.secretsMountPoint}/$_agenix_generation"
chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation"
'';
rootOwnedSecrets = builtins.filter (st: st.owner == "root" && st.group == "root") (builtins.attrValues cfg.secrets);
installRootOwnedSecrets = builtins.concatStringsSep "\n" (["echo '[agenix] decrypting root secrets...'"] ++ (map installSecret rootOwnedSecrets));
chownGroup =
if isDarwin
then "admin"
else "keys";
# chown the secrets mountpoint and the current generation to the keys group
# instead of leaving it root:root.
chownMountPoint = ''
chown :${chownGroup} "${cfg.secretsMountPoint}" "${cfg.secretsMountPoint}/$_agenix_generation"
'';
nonRootSecrets = builtins.filter (st: st.owner != "root" && st.group != "root") (builtins.attrValues cfg.secrets);
installNonRootSecrets = builtins.concatStringsSep "\n" (["echo '[agenix] decrypting non-root secrets...'"] ++ (map installSecret nonRootSecrets));
setTruePath = secretType: ''
${
if secretType.symlink
then ''
_truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}"
''
else ''
_truePath="${secretType.path}"
''
}
'';
secretType = types.submodule ({ config, ... }: {
installSecret = secretType: ''
${setTruePath secretType}
echo "decrypting '${secretType.file}' to '$_truePath'..."
TMP_FILE="$_truePath.tmp"
IDENTITIES=()
for identity in ${toString cfg.identityPaths}; do
test -r "$identity" || continue
test -s "$identity" || continue
IDENTITIES+=(-i)
IDENTITIES+=("$identity")
done
test "''${#IDENTITIES[@]}" -eq 0 && echo "[agenix] WARNING: no readable identities found!"
mkdir -p "$(dirname "$_truePath")"
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && mkdir -p "$(dirname "${secretType.path}")"
(
umask u=r,g=,o=
test -f "${secretType.file}" || echo '[agenix] WARNING: encrypted file ${secretType.file} does not exist!'
test -d "$(dirname "$TMP_FILE")" || echo "[agenix] WARNING: $(dirname "$TMP_FILE") does not exist!"
LANG=${config.i18n.defaultLocale or "C"} ${ageBin} --decrypt "''${IDENTITIES[@]}" -o "$TMP_FILE" "${secretType.file}"
)
chmod ${secretType.mode} "$TMP_FILE"
mv -f "$TMP_FILE" "$_truePath"
${optionalString secretType.symlink ''
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfn "${cfg.secretsDir}/${secretType.name}" "${secretType.path}"
''}
'';
testIdentities =
map
(path: ''
test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!'
'')
cfg.identityPaths;
cleanupAndLink = ''
_agenix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)"
(( ++_agenix_generation ))
echo "[agenix] symlinking new secrets to ${cfg.secretsDir} (generation $_agenix_generation)..."
ln -sfn "${cfg.secretsMountPoint}/$_agenix_generation" ${cfg.secretsDir}
(( _agenix_generation > 1 )) && {
echo "[agenix] removing old secrets (generation $(( _agenix_generation - 1 )))..."
rm -rf "${cfg.secretsMountPoint}/$(( _agenix_generation - 1 ))"
}
'';
installSecrets = builtins.concatStringsSep "\n" (
["echo '[agenix] decrypting secrets...'"]
++ testIdentities
++ (map installSecret (builtins.attrValues cfg.secrets))
++ [cleanupAndLink]
);
chownSecret = secretType: ''
${setTruePath secretType}
chown ${secretType.owner}:${secretType.group} "$_truePath"
'';
chownSecrets = builtins.concatStringsSep "\n" (
["echo '[agenix] chowning...'"]
++ [chownMountPoint]
++ (map chownSecret (builtins.attrValues cfg.secrets))
);
secretType = types.submodule ({config, ...}: {
options = {
name = mkOption {
type = types.str;
default = config._module.args.name;
defaultText = literalExpression "config._module.args.name";
description = ''
Name of the file used in /run/secrets
Name of the file used in {option}`age.secretsDir`
'';
};
file = mkOption {
@ -42,37 +146,58 @@ let
'';
};
path = mkOption {
type = types.str;
default = "/run/secrets/${config.name}";
description = ''
Path where the decrypted secret is installed.
'';
};
type = types.str;
default = "${cfg.secretsDir}/${config.name}";
defaultText = literalExpression ''
"''${cfg.secretsDir}/''${config.name}"
'';
description = ''
Path where the decrypted secret is installed.
'';
};
mode = mkOption {
type = types.str;
default = "0400";
description = ''
Permissions mode of the in octal.
Permissions mode of the decrypted secret in a format understood by chmod.
'';
};
owner = mkOption {
type = types.str;
default = "root";
default = "0";
description = ''
User of the file.
User of the decrypted secret.
'';
};
group = mkOption {
type = types.str;
default = users.${config.owner}.group;
default = users.${config.owner}.group or "0";
defaultText = literalExpression ''
users.''${config.owner}.group or "0"
'';
description = ''
Group of the file.
Group of the decrypted secret.
'';
};
symlink = mkEnableOption "symlinking secrets to their destination" // {default = true;};
};
});
in {
imports = [
(mkRenamedOptionModule ["age" "sshKeyPaths"] ["age" "identityPaths"])
];
options.age = {
ageBin = mkOption {
type = types.str;
default = "${pkgs.age}/bin/age";
defaultText = literalExpression ''
"''${pkgs.age}/bin/age"
'';
description = ''
The age executable to use.
'';
};
secrets = mkOption {
type = types.attrsOf secretType;
default = {};
@ -80,28 +205,116 @@ in {
Attrset of secrets.
'';
};
sshKeyPaths = mkOption {
secretsDir = mkOption {
type = types.path;
default = "/run/agenix";
description = ''
Folder where secrets are symlinked to
'';
};
secretsMountPoint = mkOption {
type =
types.addCheck types.str
(s:
(builtins.match "[ \t\n]*" s)
== null # non-empty
&& (builtins.match ".+/" s) == null) # without trailing slash
// {description = "${types.str.description} (with check: non-empty without trailing slash)";};
default = "/run/agenix.d";
description = ''
Where secrets are created before they are symlinked to {option}`age.secretsDir`
'';
};
identityPaths = mkOption {
type = types.listOf types.path;
default = if config.services.openssh.enable then
map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys)
else [];
default =
if (config.services.openssh.enable or false)
then map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys)
else if isDarwin
then [
"/etc/ssh/ssh_host_ed25519_key"
"/etc/ssh/ssh_host_rsa_key"
]
else [];
defaultText = literalExpression ''
if (config.services.openssh.enable or false)
then map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys)
else if isDarwin
then [
"/etc/ssh/ssh_host_ed25519_key"
"/etc/ssh/ssh_host_rsa_key"
]
else [];
'';
description = ''
Path to SSH keys to be used as identities in age decryption.
'';
};
};
config = mkIf (cfg.secrets != {}) {
assertions = [{
assertion = cfg.sshKeyPaths != [];
message = "age.sshKeyPaths must be set.";
}];
# Secrets with root owner and group can be installed before users
# exist. This allows user password files to be encrypted.
system.activationScripts.agenixRoot = installRootOwnedSecrets;
system.activationScripts.users.deps = [ "agenixRoot" ];
config = mkIf (cfg.secrets != {}) (mkMerge [
{
assertions = [
{
assertion = cfg.identityPaths != [];
message = "age.identityPaths must be set.";
}
];
}
# Other secrets need to wait for users and groups to exist.
system.activationScripts.agenix = stringAfter [ "users" "groups" ] installNonRootSecrets;
};
(optionalAttrs (!isDarwin) {
# Create a new directory full of secrets for symlinking (this helps
# ensure removed secrets are actually removed, or at least become
# invalid symlinks).
system.activationScripts.agenixNewGeneration = {
text = newGeneration;
deps = [
"specialfs"
];
};
system.activationScripts.agenixInstall = {
text = installSecrets;
deps = [
"agenixNewGeneration"
"specialfs"
];
};
# So user passwords can be encrypted.
system.activationScripts.users.deps = ["agenixInstall"];
# Change ownership and group after users and groups are made.
system.activationScripts.agenixChown = {
text = chownSecrets;
deps = [
"users"
"groups"
];
};
# So other activation scripts can depend on agenix being done.
system.activationScripts.agenix = {
text = "";
deps = ["agenixChown"];
};
})
(optionalAttrs isDarwin {
launchd.daemons.activate-agenix = {
script = ''
set -e
set -o pipefail
export PATH="${pkgs.gnugrep}/bin:${pkgs.coreutils}/bin:@out@/sw/bin:/usr/bin:/bin:/usr/sbin:/sbin"
${newGeneration}
${installSecrets}
${chownSecrets}
exit 0
'';
serviceConfig = {
RunAtLoad = true;
KeepAlive.SuccessfulExit = false;
};
};
})
]);
}

3
overlay.nix Normal file
View File

@ -0,0 +1,3 @@
final: prev: {
agenix = prev.callPackage ./pkgs/agenix.nix {};
}

View File

@ -1,155 +1,62 @@
{writeShellScriptBin, runtimeShell, pkgs} :
let
rage = pkgs.callPackage ./rage.nix {};
ageBin = "${rage}/bin/rage";
{
lib,
stdenv,
age,
jq,
nix,
mktemp,
diffutils,
substituteAll,
ageBin ? "${age}/bin/age",
shellcheck,
}: let
bin = "${placeholder "out"}/bin/agenix";
in
writeShellScriptBin "agenix" ''
set -Eeuo pipefail
stdenv.mkDerivation rec {
pname = "agenix";
version = "0.15.0";
src = substituteAll {
inherit ageBin version;
jqBin = "${jq}/bin/jq";
nixInstantiate = "${nix}/bin/nix-instantiate";
mktempBin = "${mktemp}/bin/mktemp";
diffBin = "${diffutils}/bin/diff";
src = ./agenix.sh;
};
dontUnpack = true;
doInstallCheck = true;
installCheckInputs = [shellcheck];
postInstallCheck = ''
shellcheck ${bin}
${bin} -h | grep ${version}
PACKAGE="agenix"
HOME=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
function cleanup {
rm -rf $HOME
}
trap "cleanup" 0 2 3 15
function show_help () {
echo "$PACKAGE - edit and rekey age secret files"
echo " "
echo "$PACKAGE -e FILE [-i PRIVATE_KEY]"
echo "$PACKAGE -r [-i PRIVATE_KEY]"
echo ' '
echo 'options:'
echo '-h, --help show help'
echo '-e, --edit FILE edits FILE using $EDITOR'
echo '-r, --rekey re-encrypts all secrets with specified recipients'
echo '-i, --identity identity to use when decrypting'
echo ' '
echo 'FILE an age-encrypted file'
echo ' '
echo 'PRIVATE_KEY a path to a private SSH key used to decrypt file'
echo ' '
echo 'EDITOR environment variable of editor to use when editing FILE'
echo ' '
echo 'RULES environment variable with path to Nix file specifying recipient public keys.'
echo "Defaults to './secrets.nix'"
echo ' '
echo "age binary path: ${ageBin}"
echo "age version: $(${ageBin} --version)"
}
mkdir -p $HOME/.ssh
cp -r "${../example}" $HOME/secrets
chmod -R u+rw $HOME/secrets
(
umask u=rw,g=r,o=r
cp ${../example_keys/user1.pub} $HOME/.ssh/id_ed25519.pub
chown $UID $HOME/.ssh/id_ed25519.pub
)
(
umask u=rw,g=,o=
cp ${../example_keys/user1} $HOME/.ssh/id_ed25519
chown $UID $HOME/.ssh/id_ed25519
)
test $# -eq 0 && (show_help && exit 1)
cd $HOME/secrets
test $(${bin} -d secret1.age) = "hello"
'';
REKEY=0
DEFAULT_DECRYPT=(--decrypt)
installPhase = ''
install -D $src ${bin}
'';
while test $# -gt 0; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-e|--edit)
shift
if test $# -gt 0; then
export FILE=$1
else
echo "no FILE specified"
exit 1
fi
shift
;;
-i|--identity)
shift
if test $# -gt 0; then
DEFAULT_DECRYPT+=(--identity "$1")
else
echo "no PRIVATE_KEY specified"
exit 1
fi
shift
;;
-r|--rekey)
shift
REKEY=1
;;
*)
show_help
exit 1
;;
esac
done
RULES=''${RULES:-./secrets.nix}
function cleanup {
if [ ! -z ''${CLEARTEXT_DIR+x} ]
then
rm -rf "$CLEARTEXT_DIR"
fi
if [ ! -z ''${REENCRYPTED_DIR+x} ]
then
rm -rf "$REENCRYPTED_DIR"
fi
}
trap "cleanup" 0 2 3 15
function edit {
FILE=$1
KEYS=$((nix-instantiate --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" rules.\"$FILE\".publicKeys)" | sed 's/"//g' | sed 's/\\n/\n/g') || exit 1)
if [ -z "$KEYS" ]
then
>&2 echo "There is no rule for $FILE in $RULES."
exit 1
fi
CLEARTEXT_DIR=$(mktemp -d)
CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")"
if [ -f "$FILE" ]
then
DECRYPT=("''${DEFAULT_DECRYPT[@]}")
while IFS= read -r key
do
DECRYPT+=(--identity "$key")
done <<<"$((find ~/.ssh -maxdepth 1 -type f -not -name "*pub" -not -name "config" -not -name "authorized_keys" -not -name "known_hosts") || exit 1)"
DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE")
${ageBin} "''${DECRYPT[@]}" || exit 1
cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before"
fi
$EDITOR "$CLEARTEXT_FILE"
if [ ! -f "$CLEARTEXT_FILE" ]
then
echo "$FILE wasn't created."
return
fi
[ -f "$FILE" ] && [ "$EDITOR" != ":" ] && diff "$CLEARTEXT_FILE.before" "$CLEARTEXT_FILE" 1>/dev/null && echo "$FILE wasn't changed, skipping re-encryption." && return
ENCRYPT=()
while IFS= read -r key
do
ENCRYPT+=(--recipient "$key")
done <<< "$KEYS"
REENCRYPTED_DIR=$(mktemp -d)
REENCRYPTED_FILE="$REENCRYPTED_DIR/$(basename "$FILE")"
ENCRYPT+=(-o "$REENCRYPTED_FILE")
${ageBin} "''${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1
mv -f "$REENCRYPTED_FILE" "$1"
}
function rekey {
FILES=$((nix-instantiate --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" (builtins.attrNames rules))" | sed 's/"//g' | sed 's/\\n/\n/g') || exit 1)
for FILE in $FILES
do
echo "rekeying $FILE..."
EDITOR=: edit "$FILE"
cleanup
done
}
[ $REKEY -eq 1 ] && rekey && exit 0
edit "$FILE" && cleanup && exit 0
''
meta.description = "age-encrypted secrets for NixOS";
}

204
pkgs/agenix.sh Normal file
View File

@ -0,0 +1,204 @@
#!/usr/bin/env bash
set -Eeuo pipefail
PACKAGE="agenix"
function show_help () {
echo "$PACKAGE - edit and rekey age secret files"
echo " "
echo "$PACKAGE -e FILE [-i PRIVATE_KEY]"
echo "$PACKAGE -r [-i PRIVATE_KEY]"
echo ' '
echo 'options:'
echo '-h, --help show help'
# shellcheck disable=SC2016
echo '-e, --edit FILE edits FILE using $EDITOR'
echo '-r, --rekey re-encrypts all secrets with specified recipients'
echo '-d, --decrypt FILE decrypts FILE to STDOUT'
echo '-i, --identity identity to use when decrypting'
echo '-v, --verbose verbose output'
echo ' '
echo 'FILE an age-encrypted file'
echo ' '
echo 'PRIVATE_KEY a path to a private SSH key used to decrypt file'
echo ' '
echo 'EDITOR environment variable of editor to use when editing FILE'
echo ' '
echo 'If STDIN is not interactive, EDITOR will be set to "cp /dev/stdin"'
echo ' '
echo 'RULES environment variable with path to Nix file specifying recipient public keys.'
echo "Defaults to './secrets.nix'"
echo ' '
echo "agenix version: @version@"
echo "age binary path: @ageBin@"
echo "age version: $(@ageBin@ --version)"
}
function warn() {
printf '%s\n' "$*" >&2
}
function err() {
warn "$*"
exit 1
}
test $# -eq 0 && (show_help && exit 1)
REKEY=0
DECRYPT_ONLY=0
DEFAULT_DECRYPT=(--decrypt)
while test $# -gt 0; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-e|--edit)
shift
if test $# -gt 0; then
export FILE=$1
else
echo "no FILE specified"
exit 1
fi
shift
;;
-i|--identity)
shift
if test $# -gt 0; then
DEFAULT_DECRYPT+=(--identity "$1")
else
echo "no PRIVATE_KEY specified"
exit 1
fi
shift
;;
-r|--rekey)
shift
REKEY=1
;;
-d|--decrypt)
shift
DECRYPT_ONLY=1
if test $# -gt 0; then
export FILE=$1
else
echo "no FILE specified"
exit 1
fi
shift
;;
-v|--verbose)
shift
set -x
;;
*)
show_help
exit 1
;;
esac
done
RULES=${RULES:-./secrets.nix}
function cleanup {
if [ -n "${CLEARTEXT_DIR+x}" ]
then
rm -rf "$CLEARTEXT_DIR"
fi
if [ -n "${REENCRYPTED_DIR+x}" ]
then
rm -rf "$REENCRYPTED_DIR"
fi
}
trap "cleanup" 0 2 3 15
function keys {
(@nixInstantiate@ --json --eval --strict -E "(let rules = import $RULES; in rules.\"$1\".publicKeys)" | @jqBin@ -r .[]) || exit 1
}
function decrypt {
FILE=$1
KEYS=$2
if [ -z "$KEYS" ]
then
err "There is no rule for $FILE in $RULES."
fi
if [ -f "$FILE" ]
then
DECRYPT=("${DEFAULT_DECRYPT[@]}")
if [[ "${DECRYPT[*]}" != *"--identity"* ]]; then
if [ -f "$HOME/.ssh/id_rsa" ]; then
DECRYPT+=(--identity "$HOME/.ssh/id_rsa")
fi
if [ -f "$HOME/.ssh/id_ed25519" ]; then
DECRYPT+=(--identity "$HOME/.ssh/id_ed25519")
fi
fi
if [[ "${DECRYPT[*]}" != *"--identity"* ]]; then
err "No identity found to decrypt $FILE. Try adding an SSH key at $HOME/.ssh/id_rsa or $HOME/.ssh/id_ed25519 or using the --identity flag to specify a file."
fi
@ageBin@ "${DECRYPT[@]}" "$FILE" || exit 1
fi
}
function edit {
FILE=$1
KEYS=$(keys "$FILE") || exit 1
CLEARTEXT_DIR=$(@mktempBin@ -d)
CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")"
DEFAULT_DECRYPT+=(-o "$CLEARTEXT_FILE")
decrypt "$FILE" "$KEYS" || exit 1
[ ! -f "$CLEARTEXT_FILE" ] || cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before"
[ -t 0 ] || EDITOR='cp /dev/stdin'
$EDITOR "$CLEARTEXT_FILE"
if [ ! -f "$CLEARTEXT_FILE" ]
then
warn "$FILE wasn't created."
return
fi
[ -f "$FILE" ] && [ "$EDITOR" != ":" ] && @diffBin@ -q "$CLEARTEXT_FILE.before" "$CLEARTEXT_FILE" && warn "$FILE wasn't changed, skipping re-encryption." && return
ENCRYPT=()
while IFS= read -r key
do
if [ -n "$key" ]; then
ENCRYPT+=(--recipient "$key")
fi
done <<< "$KEYS"
REENCRYPTED_DIR=$(@mktempBin@ -d)
REENCRYPTED_FILE="$REENCRYPTED_DIR/$(basename "$FILE")"
ENCRYPT+=(-o "$REENCRYPTED_FILE")
@ageBin@ "${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1
mkdir -p "$(dirname "$FILE")"
mv -f "$REENCRYPTED_FILE" "$FILE"
}
function rekey {
FILES=$( (@nixInstantiate@ --json --eval -E "(let rules = import $RULES; in builtins.attrNames rules)" | @jqBin@ -r .[]) || exit 1)
for FILE in $FILES
do
warn "rekeying $FILE..."
EDITOR=: edit "$FILE"
cleanup
done
}
[ $REKEY -eq 1 ] && rekey && exit 0
[ $DECRYPT_ONLY -eq 1 ] && DEFAULT_DECRYPT+=("-o" "-") && decrypt "${FILE}" "$(keys "$FILE")" && exit 0
edit "$FILE" && cleanup && exit 0

11
pkgs/doc.nix Normal file
View File

@ -0,0 +1,11 @@
{
stdenvNoCC,
mmdoc,
self,
}:
stdenvNoCC.mkDerivation rec {
name = "agenix-doc";
src = ../doc;
phases = ["mmdocPhase"];
mmdocPhase = "${mmdoc}/bin/mmdoc agenix $src $out";
}

View File

@ -1,37 +0,0 @@
{stdenv, rustPlatform, fetchFromGitHub, installShellFiles, darwin }:
rustPlatform.buildRustPackage rec {
pname = "rage";
version = "unstable-2020-09-05";
src = fetchFromGitHub {
owner = "str4d";
repo = pname;
rev = "8368992e60cbedb2d6b725c3e25440e65d8544d1";
sha256 = "sha256-ICcApZQrR4hGxo/RcFMktenE4dswAXA2/nJ5D++O2ig=";
};
cargoSha256 = "sha256-QwNtp7Hxsiads3bh8NRra25RdPbIdjp+pSWTllAvdmQ=";
nativeBuildInputs = [ installShellFiles ];
buildInputs = stdenv.lib.optionals stdenv.isDarwin [ darwin.Security ];
postBuild = ''
cargo run --example generate-docs
cargo run --example generate-completions
'';
postInstall = ''
installManPage target/manpages/*
installShellCompletion target/completions/*.{bash,fish,zsh}
'';
meta = with stdenv.lib; {
description = "A simple, secure and modern encryption tool with small explicit keys, no config options, and UNIX-style composability";
homepage = "https://github.com/str4d/rage";
changelog = "https://github.com/str4d/rage/releases/tag/v${version}";
license = licenses.asl20;
maintainers = [ maintainers.marsam ];
};
}

View File

@ -0,0 +1,28 @@
# Do not copy this! It is insecure. This is only okay because we are testing.
{config, ...}: {
system.activationScripts.agenixInstall.deps = ["installSSHHostKeys"];
system.activationScripts.installSSHHostKeys.text = ''
USER1_UID="${toString config.users.users.user1.uid}"
USERS_GID="${toString config.users.groups.users.gid}"
mkdir -p /etc/ssh /home/user1/.ssh
chown $USER1_UID:$USERS_GID /home/user1/.ssh
(
umask u=rw,g=r,o=r
cp ${../example_keys/system1.pub} /etc/ssh/ssh_host_ed25519_key.pub
cp ${../example_keys/user1.pub} /home/user1/.ssh/id_ed25519.pub
chown $USER1_UID:$USERS_GID /home/user1/.ssh/id_ed25519.pub
)
(
umask u=rw,g=,o=
cp ${../example_keys/system1} /etc/ssh/ssh_host_ed25519_key
cp ${../example_keys/user1} /home/user1/.ssh/id_ed25519
chown $USER1_UID:$USERS_GID /home/user1/.ssh/id_ed25519
touch /etc/ssh/ssh_host_rsa_key
)
cp -r "${../example}" /tmp/secrets
chmod -R u+rw /tmp/secrets
chown -R $USER1_UID:$USERS_GID /tmp/secrets
'';
}

View File

@ -0,0 +1,17 @@
# Do not copy this! It is insecure. This is only okay because we are testing.
{
system.activationScripts.extraUserActivation.text = ''
echo "Installing system SSH host key"
sudo cp ${../example_keys/system1.pub} /etc/ssh/ssh_host_ed25519_key.pub
sudo cp ${../example_keys/system1} /etc/ssh/ssh_host_ed25519_key
sudo chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
sudo chmod 600 /etc/ssh/ssh_host_ed25519_key
echo "Installing user SSH host key"
mkdir -p $HOME/.ssh
cp ${../example_keys/user1.pub} $HOME/.ssh/id_ed25519.pub
cp ${../example_keys/user1} $HOME/.ssh/id_ed25519
chmod 644 $HOME/.ssh/id_ed25519.pub
chmod 600 $HOME/.ssh/id_ed25519
'';
}

126
test/integration.nix Normal file
View File

@ -0,0 +1,126 @@
{
nixpkgs ? <nixpkgs>,
pkgs ?
import <nixpkgs> {
inherit system;
config = {};
},
system ? builtins.currentSystem,
home-manager ? <home-manager>,
}:
pkgs.nixosTest {
name = "agenix-integration";
nodes.system1 = {
config,
pkgs,
options,
...
}: {
imports = [
../modules/age.nix
./install_ssh_host_keys.nix
"${home-manager}/nixos"
];
services.openssh.enable = true;
age.secrets.passwordfile-user1 = {
file = ../example/passwordfile-user1.age;
};
age.identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
environment.systemPackages = [
(pkgs.callPackage ../pkgs/agenix.nix {})
];
users = {
mutableUsers = false;
users = {
user1 = {
isNormalUser = true;
passwordFile = config.age.secrets.passwordfile-user1.path;
uid = 1000;
};
};
};
home-manager.users.user1 = {options, ...}: {
imports = [
../modules/age-home.nix
];
home.stateVersion = pkgs.lib.trivial.release;
age = {
identityPaths = options.age.identityPaths.default ++ ["/home/user1/.ssh/this_key_wont_exist"];
secrets.secret2 = {
# Only decryptable by user1's key
file = ../example/secret2.age;
};
secrets.secret2Path = {
file = ../example/secret2.age;
path = "/home/user1/secret2";
};
};
};
};
testScript = let
user = "user1";
password = "password1234";
secret2 = "world!";
in ''
system1.wait_for_unit("multi-user.target")
system1.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
system1.sleep(2)
system1.send_key("alt-f2")
system1.wait_until_succeeds("[ $(fgconsole) = 2 ]")
system1.wait_for_unit("getty@tty2.service")
system1.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
system1.wait_until_tty_matches("2", "login: ")
system1.send_chars("${user}\n")
system1.wait_until_tty_matches("2", "login: ${user}")
system1.wait_until_succeeds("pgrep login")
system1.sleep(2)
system1.send_chars("${password}\n")
system1.send_chars("whoami > /tmp/1\n")
system1.wait_for_file("/tmp/1")
assert "${user}" in system1.succeed("cat /tmp/1")
system1.send_chars("cat /run/user/$(id -u)/agenix/secret2 > /tmp/2\n")
system1.wait_for_file("/tmp/2")
assert "${secret2}" in system1.succeed("cat /tmp/2")
userDo = lambda input : f"sudo -u user1 -- bash -c 'set -eou pipefail; cd /tmp/secrets; {input}'"
before_hash = system1.succeed(userDo('sha256sum passwordfile-user1.age')).split()
print(system1.succeed(userDo('agenix -r -i /home/user1/.ssh/id_ed25519')))
after_hash = system1.succeed(userDo('sha256sum passwordfile-user1.age')).split()
# Ensure we actually have hashes
for h in [before_hash, after_hash]:
assert len(h) == 2, "hash should be [hash, filename]"
assert h[1] == "passwordfile-user1.age", "filename is incorrect"
assert len(h[0].strip()) == 64, "hash length is incorrect"
assert before_hash[0] != after_hash[0], "hash did not change with rekeying"
# user1 can edit passwordfile-user1.age
system1.succeed(userDo("EDITOR=cat agenix -e passwordfile-user1.age"))
# user1 can edit even if bogus id_rsa present
system1.succeed(userDo("echo bogus > ~/.ssh/id_rsa"))
system1.fail(userDo("EDITOR=cat agenix -e passwordfile-user1.age"))
system1.succeed(userDo("EDITOR=cat agenix -e passwordfile-user1.age -i /home/user1/.ssh/id_ed25519"))
system1.succeed(userDo("rm ~/.ssh/id_rsa"))
# user1 can edit a secret by piping in contents
system1.succeed(userDo("echo 'secret1234' | agenix -e passwordfile-user1.age"))
# and get it back out via --decrypt
assert "secret1234" in system1.succeed(userDo("agenix -d passwordfile-user1.age"))
# finally, the plain text should not linger around anywhere in the filesystem.
system1.fail("grep -r secret1234 /tmp")
'';
}

View File

@ -0,0 +1,28 @@
{
config,
pkgs,
options,
...
}: let
secret = "hello";
testScript = pkgs.writeShellApplication {
name = "agenix-integration";
text = ''
grep "${secret}" "${config.age.secrets.system-secret.path}"
'';
};
in {
imports = [
./install_ssh_host_keys_darwin.nix
../modules/age.nix
];
services.nix-daemon.enable = true;
age = {
identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
secrets.system-secret.file = ../example/secret1.age;
};
environment.systemPackages = [testScript];
}

View File

@ -0,0 +1,33 @@
{
pkgs,
config,
options,
lib,
...
}: {
imports = [../modules/age-home.nix];
age = {
identityPaths = options.age.identityPaths.default ++ ["/Users/user1/.ssh/this_key_wont_exist"];
secrets.user-secret.file = ../example/secret2.age;
};
home = rec {
username = "runner";
homeDirectory = lib.mkForce "/Users/${username}";
stateVersion = lib.trivial.release;
};
home.file = let
name = "agenix-home-integration";
in {
${name}.source = pkgs.writeShellApplication {
inherit name;
text = let
secret = "world!";
in ''
diff -q "${config.age.secrets.user-secret.path}" <(printf '${secret}\n')
'';
};
};
}