Compare commits

..

112 commits

Author SHA1 Message Date
Ryan Mulligan
f6291c5935
Merge pull request #280 from Kreyren/patch-3
age-home: Use curly-brackets for XDG_RUNTIME_DIR
2024-08-10 05:45:04 -07:00
Jacob Hrbek
e3413992fb age-home: Use curly-brackets for XDG_RUNTIME_DIR
To avoid having to do 4fd99eae63/nixos/secrets.nix (L25C9-L29C116) while using agenix in user services.
2024-08-10 05:05:53 +02:00
Ryan Mulligan
3f1dae074a
Merge pull request #277 from fzakaria/import-module-remove
Remove import for NixOS/HM modules
2024-07-30 04:30:03 -07:00
Farid Zakaria
40012e5ed4 Remove import for NixOS/HM modules
When using `files` on a NixOS option in the `nix repl` it fails to
follow the attribute of agenix module.

Discussing with @roberth has explained that this is a "common bug" on
account of mis-using the `import` for modules.

From what I understand, the `import` statement brings it into the
current context so you lose the attribute of where it's defined.

Here is what I currently see:
```
nix-repl> options.age.ageBin.files
[
  "/nix/store/8kpmdb63f5i9mwdyirqki7hvvglgy1va-source/machines/nyx/configuration.nix"
]

```

After this change, the value in agenix is reported instead.
```
❯ nix repl --extra-experimental-features 'flakes repl-flake' \
           --override-input agenix /home/fmzakari/code/github.com/ryantm/agenix .

nix-repl> options.age.ageBin.files
[
  "/nix/store/99gc8rhgw43k201k34pshcsygdvbhmpy-source/modules/age.nix"
]
```
2024-07-29 08:15:01 -07:00
Ryan Mulligan
de96bd907d
Merge pull request #265 from Kreyren/patch-1
README: Add warning about HNDL and PQS in threat model
2024-07-09 10:30:05 -07:00
KREYREN
760751b6d1
README: Add warning about HNDL and PQS in theat model 2024-06-19 15:37:53 +00:00
Ryan Mulligan
3a56735779
Merge pull request #187 from oddlama/main
fix: always treat link destinations as files to ensure an error when the destination is a directory
2024-06-14 06:18:04 -07:00
Nathan Henrie
c2fc0762bb
Merge pull request #241 from sternenseemann/nix-2.3-install-check
agenix: fix installCheckPhase with Nix 2.3
2024-05-24 08:40:46 -06:00
oddlama
08ed896eb6
fix: always treat link destinations as files to ensure error when destination is a directory.
This can happen if for example a secret is used in the initrd, which
materializes it as a directory, which then causes agenix to silently
create an incorrect link when switching to stage2. This ensures that
agenix will abort with an error.
2024-05-21 15:08:15 +02:00
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
sternenseemann
1746e4f5ec agenix: fix installCheckPhase with Nix 2.3
As opposed to e.g. Nix 2.18, Nix 2.3 doesn't try to create a fallback
store in $HOME if $NIX_STORE_DIR and $NIX_STATE_DIR aren't writable.
2024-02-01 13:30:22 +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
b0721be0c6 doc: how to skip the Darwin input 2023-02-19 15:12:18 -08:00
37 changed files with 1395 additions and 345 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

View file

@ -7,24 +7,45 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v18
- uses: cachix/install-nix-action@v22
with:
extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm"
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-11
runs-on: macos-12
steps:
- uses: actions/checkout@v3
- uses: cachix/install-nix-action@v18
- uses: cachix/install-nix-action@v24
with:
extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm"
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
- run: |
system=$(nix build --no-link --print-out-paths .#checks.x86_64-darwin.integration)
${system}/activate-user
sudo ${system}/activate
- run: sudo /run/current-system/sw/bin/agenix-integration
- 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 }}

118
README.md
View file

@ -1,6 +1,14 @@
# 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
@ -37,7 +45,7 @@ All files in the Nix store are readable by any system user, so it is not a suita
## Notices
* Password-protected ssh keys: since the underlying tool age/rage do 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.
* 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
@ -172,6 +180,8 @@ To install the `agenix` binary:
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
@ -189,13 +199,14 @@ To install the `agenix` binary:
#### 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
{
@ -203,6 +214,28 @@ but, if you want to (change the system based on your system):
}
```
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
@ -211,14 +244,15 @@ but, if you want to (change the system based on your system):
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.):
2. Make a directory to store secrets and `secrets.nix` file for listing secrets and their public keys:
```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)):
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";
@ -234,17 +268,32 @@ but, if you want to (change the system based on your system):
"secret2.age".publicKeys = users ++ systems;
}
```
4. 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
```
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;
}
```
6. Use the secret in your config:
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.
6. Reference the secrets' mount path in your config:
```nix
{
users.users.user1 = {
@ -253,9 +302,22 @@ but, if you want to (change the system based on your system):
};
}
```
7. NixOS rebuild or use your deployment tool like usual.
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 secret will be decrypted to the value of `config.age.secrets.secret1.path` (`/run/agenix/secret1` by default).
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
@ -383,7 +445,7 @@ Example:
#### `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
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
@ -425,7 +487,7 @@ Example of a secret with a name different from its attrpath:
#### `age.ageBin`
`age.ageBin` the string of the path to the `age` binary. Usually, you
don't need to change this. Defaults to `rage/bin/rage`.
don't need to change this. Defaults to `age/bin/age`.
Overriding `age.ageBin` example:
@ -437,13 +499,14 @@ Overriding `age.ageBin` example:
#### `age.identityPaths`
`age.identityPaths` is a list of paths to recipient keys to try to use
to decrypt the secrets. All of the file paths must be present, but
only one needs to be able to decrypt the secret. Usually, you don't
need to change this. By default, this is the `rsa` and `ed25519` keys
in `config.services.openssh.hostKeys`.
Overriding `age.identityPaths` example:
`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
{
@ -454,7 +517,7 @@ Overriding `age.identityPaths` example:
#### `age.secretsDir`
`age.secretsDir` is the directory where secrets are symlinked to by
default.Usually, you don't need to change this. Defaults to
default. Usually, you don't need to change this. Defaults to
`/run/agenix`.
Overriding `age.secretsDir` example:
@ -483,6 +546,8 @@ Overriding `age.secretsMountPoint` example:
### agenix CLI reference
```
agenix - edit and rekey age secret files
agenix -e FILE [-i PRIVATE_KEY]
agenix -r [-i PRIVATE_KEY]
@ -490,6 +555,7 @@ 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
@ -499,6 +565,8 @@ 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'
```
@ -519,13 +587,13 @@ improved upon by reading the identities from the age file.)
#### Overriding age binary
The agenix CLI uses `rage` by default as its age implemenation, you
can use the reference implementation `age` with Flakes like this:
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.age}/bin/age"; })
(agenix.packages.x86_64-linux.default.override { ageBin = "${pkgs.rage}/bin/rage"; })
];
}
```
@ -550,6 +618,8 @@ 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`.
Additionally you should only encrypt secrets that you are able to make useless in the event that they are decrypted in the future and be ready to rotate them periodically as [age](https://github.com/FiloSottile/age) is [as of 19th June 2024 NOT Post-Quantum Safe](https://github.com/FiloSottile/age/discussions/231#discussioncomment-3092773) and so in case the threat actor can access your encrypted keys e.g. via their use in a public repository then they can utilize the strategy of [Harvest Now, Decrypt Later](https://en.wikipedia.org/wiki/Harvest_now,_decrypt_later) to store your keys now for later decryption including the case where a major vulnerability is found that would expose the secrets. See https://github.com/FiloSottile/age/issues/578 for details.
## Contributing
* The main branch is protected against direct pushes

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

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1673295039,
"narHash": "sha256-AsdYgE8/GPwcelGgrntlijMg4t3hLFJFCRF3tL5WVjA=",
"lastModified": 1700795494,
"narHash": "sha256-gzGLZSiOhf155FW7262kdHo2YDeugp3VuIFb4/GGng0=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "87b9d090ad39b25b2400029c64825fc2a8868943",
"rev": "4b9b83d5a92e8c1fbfd8eb27eda375908c11ec4d",
"type": "github"
},
"original": {
@ -21,13 +21,33 @@
"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": 1674641431,
"narHash": "sha256-qfo19qVZBP4qn5M5gXc/h1MDgAtPA5VxJm9s8RUAkVk=",
"lastModified": 1703013332,
"narHash": "sha256-+tFNwMvlXLbJZXiMHqYq77z/RfmpfpiI3yjL6o/Zo9M=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9b97ad7b4330aacda9b2343396eb3df8a853b4fc",
"rev": "54aac082a4d9bb5bbc5c4e899603abfb76a3f6d6",
"type": "github"
},
"original": {
@ -40,7 +60,24 @@
"root": {
"inputs": {
"darwin": "darwin",
"nixpkgs": "nixpkgs"
"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

@ -7,60 +7,83 @@
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,
darwin,
home-manager,
systems,
}: let
agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {};
eachSystem = nixpkgs.lib.genAttrs (import systems);
in {
nixosModules.age = import ./modules/age.nix;
nixosModules.age = ./modules/age.nix;
nixosModules.default = self.nixosModules.age;
darwinModules.age = import ./modules/age.nix;
darwinModules.age = ./modules/age.nix;
darwinModules.default = self.darwinModules.age;
homeManagerModules.age = ./modules/age-home.nix;
homeManagerModules.default = self.homeManagerModules.age;
overlays.default = import ./overlay.nix;
formatter.x86_64-darwin = nixpkgs.legacyPackages.x86_64-darwin.alejandra;
packages.x86_64-darwin.agenix = agenix "x86_64-darwin";
packages.x86_64-darwin.default = self.packages.x86_64-darwin.agenix;
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
formatter.aarch64-darwin = nixpkgs.legacyPackages.aarch64-darwin.alejandra;
packages.aarch64-darwin.agenix = agenix "aarch64-darwin";
packages.aarch64-darwin.default = self.packages.aarch64-darwin.agenix;
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;
});
formatter.aarch64-linux = nixpkgs.legacyPackages.aarch64-linux.alejandra;
packages.aarch64-linux.agenix = agenix "aarch64-linux";
packages.aarch64-linux.default = self.packages.aarch64-linux.agenix;
checks =
nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: {
integration =
(darwin.lib.darwinSystem {
inherit system;
modules = [
./test/integration_darwin.nix
formatter.i686-linux = nixpkgs.legacyPackages.i686-linux.alejandra;
packages.i686-linux.agenix = agenix "i686-linux";
packages.i686-linux.default = self.packages.i686-linux.agenix;
# Allow new-style nix commands in CI
{nix.extraOptions = "experimental-features = nix-command flakes";}
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.alejandra;
packages.x86_64-linux.agenix = agenix "x86_64-linux";
packages.x86_64-linux.default = self.packages.x86_64-linux.agenix;
checks.x86_64-linux.integration = import ./test/integration.nix {
inherit nixpkgs;
pkgs = nixpkgs.legacyPackages.x86_64-linux;
system = "x86_64-linux";
};
checks."aarch64-darwin".integration =
(darwin.lib.darwinSystem {
system = "aarch64-darwin";
modules = [./test/integration_darwin.nix "${darwin.outPath}/pkgs/darwin-installer/installer.nix"];
home-manager.darwinModules.home-manager
{
home-manager = {
verbose = true;
useGlobalPkgs = true;
useUserPackages = true;
backupFileExtension = "hmbak";
users.runner = ./test/integration_hm_darwin.nix;
};
}
];
})
.system;
})
.system;
checks."x86_64-darwin".integration =
(darwin.lib.darwinSystem {
system = "x86_64-darwin";
modules = [./test/integration_darwin.nix "${darwin.outPath}/pkgs/darwin-installer/installer.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.system = self.checks."x86_64-darwin".integration;
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 -sfT "${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 -sfT "${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

@ -10,11 +10,6 @@ with lib; let
isDarwin = lib.attrsets.hasAttrByPath ["environment" "darwinConfig"] options;
# we need at least rage 0.5.0 to support ssh keys
rage =
if lib.versionOlder pkgs.rage.version "0.5.0"
then pkgs.callPackage ../pkgs/rage.nix {}
else pkgs.rage;
ageBin = config.age.ageBin;
users = config.users.users;
@ -74,6 +69,7 @@ with lib; let
IDENTITIES=()
for identity in ${toString cfg.identityPaths}; do
test -r "$identity" || continue
test -s "$identity" || continue
IDENTITIES+=(-i)
IDENTITIES+=("$identity")
done
@ -92,7 +88,7 @@ with lib; let
mv -f "$TMP_FILE" "$_truePath"
${optionalString secretType.symlink ''
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfn "${cfg.secretsDir}/${secretType.name}" "${secretType.path}"
[ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfT "${cfg.secretsDir}/${secretType.name}" "${secretType.path}"
''}
'';
@ -107,7 +103,7 @@ with lib; let
_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}
ln -sfT "${cfg.secretsMountPoint}/$_agenix_generation" ${cfg.secretsDir}
(( _agenix_generation > 1 )) && {
echo "[agenix] removing old secrets (generation $(( _agenix_generation - 1 )))..."
@ -138,8 +134,9 @@ with lib; let
name = mkOption {
type = types.str;
default = config._module.args.name;
defaultText = literalExpression "config._module.args.name";
description = ''
Name of the file used in ''${cfg.secretsDir}
Name of the file used in {option}`age.secretsDir`
'';
};
file = mkOption {
@ -151,6 +148,9 @@ with lib; let
path = mkOption {
type = types.str;
default = "${cfg.secretsDir}/${config.name}";
defaultText = literalExpression ''
"''${cfg.secretsDir}/''${config.name}"
'';
description = ''
Path where the decrypted secret is installed.
'';
@ -172,6 +172,9 @@ with lib; let
group = mkOption {
type = types.str;
default = users.${config.owner}.group or "0";
defaultText = literalExpression ''
users.''${config.owner}.group or "0"
'';
description = ''
Group of the decrypted secret.
'';
@ -187,7 +190,10 @@ in {
options.age = {
ageBin = mkOption {
type = types.str;
default = "${rage}/bin/rage";
default = "${pkgs.age}/bin/age";
defaultText = literalExpression ''
"''${pkgs.age}/bin/age"
'';
description = ''
The age executable to use.
'';
@ -215,9 +221,8 @@ in {
&& (builtins.match ".+/" s) == null) # without trailing slash
// {description = "${types.str.description} (with check: non-empty without trailing slash)";};
default = "/run/agenix.d";
defaultText = "/run/agenix.d";
description = ''
Where secrets are created before they are symlinked to ''${cfg.secretsDir}
Where secrets are created before they are symlinked to {option}`age.secretsDir`
'';
};
identityPaths = mkOption {
@ -231,6 +236,16 @@ in {
"/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.
'';

View file

@ -1,37 +1,66 @@
{
lib,
stdenv,
rage,
gnused,
age,
jq,
nix,
mktemp,
diffutils,
substituteAll,
ageBin ? "${rage}/bin/rage",
ageBin ? "${age}/bin/age",
shellcheck,
}:
stdenv.mkDerivation rec {
pname = "agenix";
version = "0.13.0";
src = substituteAll {
inherit ageBin version;
sedBin = "${gnused}/bin/sed";
nixInstantiate = "${nix}/bin/nix-instantiate";
mktempBin = "${mktemp}/bin/mktemp";
diffBin = "${diffutils}/bin/diff";
src = ./agenix.sh;
};
dontUnpack = true;
}: let
bin = "${placeholder "out"}/bin/agenix";
in
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}
doCheck = true;
checkInputs = [shellcheck];
postCheck = ''
shellcheck $src
'';
test_tmp=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
export HOME="$test_tmp/home"
export NIX_STORE_DIR="$test_tmp/nix/store"
export NIX_STATE_DIR="$test_tmp/nix/var"
mkdir -p "$HOME" "$NIX_STORE_DIR" "$NIX_STATE_DIR"
function cleanup {
rm -rf "$test_tmp"
}
trap "cleanup" 0 2 3 15
installPhase = ''
install -D $src ${placeholder "out"}/bin/agenix
'';
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
)
meta.description = "age-encrypted secrets for NixOS";
}
cd $HOME/secrets
test $(${bin} -d secret1.age) = "hello"
'';
installPhase = ''
install -D $src ${bin}
'';
meta.description = "age-encrypted secrets for NixOS";
}

View file

@ -14,6 +14,7 @@ function 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 ' '
@ -45,6 +46,7 @@ function err() {
test $# -eq 0 && (show_help && exit 1)
REKEY=0
DECRYPT_ONLY=0
DEFAULT_DECRYPT=(--decrypt)
while test $# -gt 0; do
@ -77,6 +79,17 @@ while test $# -gt 0; do
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
@ -89,7 +102,6 @@ while test $# -gt 0; do
done
RULES=${RULES:-./secrets.nix}
function cleanup {
if [ -n "${CLEARTEXT_DIR+x}" ]
then
@ -102,18 +114,18 @@ function cleanup {
}
trap "cleanup" 0 2 3 15
function edit {
FILE=$1
KEYS=$( (@nixInstantiate@ --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" rules.\"$FILE\".publicKeys)" | @sedBin@ 's/"//g' | @sedBin@ 's/\\n/\n/g') | @sedBin@ '/^$/d' || exit 1)
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
CLEARTEXT_DIR=$(@mktempBin@ -d)
CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")"
if [ -f "$FILE" ]
then
DECRYPT=("${DEFAULT_DECRYPT[@]}")
@ -128,10 +140,22 @@ function edit {
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
DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE")
@ageBin@ "${DECRYPT[@]}" || exit 1
cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before"
@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'
@ -147,7 +171,9 @@ function edit {
ENCRYPT=()
while IFS= read -r key
do
ENCRYPT+=(--recipient "$key")
if [ -n "$key" ]; then
ENCRYPT+=(--recipient "$key")
fi
done <<< "$KEYS"
REENCRYPTED_DIR=$(@mktempBin@ -d)
@ -157,11 +183,13 @@ function edit {
@ageBin@ "${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1
mv -f "$REENCRYPTED_FILE" "$1"
mkdir -p "$(dirname "$FILE")"
mv -f "$REENCRYPTED_FILE" "$FILE"
}
function rekey {
FILES=$( (@nixInstantiate@ --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" (builtins.attrNames rules))" | @sedBin@ 's/"//g' | @sedBin@ 's/\\n/\n/g') || exit 1)
FILES=$( (@nixInstantiate@ --json --eval -E "(let rules = import $RULES; in builtins.attrNames rules)" | @jqBin@ -r .[]) || exit 1)
for FILE in $FILES
do
@ -172,4 +200,5 @@ function rekey {
}
[ $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,49 +0,0 @@
{
stdenv,
rustPlatform,
fetchFromGitHub,
installShellFiles,
darwin,
}:
rustPlatform.buildRustPackage rec {
pname = "rage";
version = "0.5.0";
src = fetchFromGitHub {
owner = "str4d";
repo = pname;
rev = "v${version}";
sha256 = "sha256-XSDfAsXfwSoe5JMdJtZlC324Sra+4fVJhE3/k2TthEc=";
};
cargoSha256 = "sha256-GPr5zxeODAjD+ynp/nned9gZUiReYcdzosuEbLIKZSs=";
nativeBuildInputs = [installShellFiles];
buildInputs = with darwin.apple_sdk.frameworks;
stdenv.lib.optionals stdenv.isDarwin [
Security
Foundation
];
# cargo test has an x86-only dependency
doCheck = stdenv.hostPlatform.isx86;
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 = with licenses; [asl20 mit]; # either at your option
maintainers = with maintainers; [marsam ryantm];
};
}

View file

@ -1,130 +0,0 @@
"""Provide a class and helper methods for agenix integration tests."""
import typing as t
T = t.TypeVar("T", str, list[str])
class AgenixTester:
"""Provide a class to help reduce repetition in setup."""
def __init__(self, system, user, password) -> None:
"""Necessary setup can be put here."""
self.system = system
self.user = user
self.password = password
self.setup()
def login(self) -> None:
self.system.wait_for_unit("multi-user.target")
self.system.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
self.system.sleep(2)
self.system.send_key("alt-f2")
self.system.wait_until_succeeds("[ $(fgconsole) = 2 ]")
self.system.wait_for_unit("getty@tty2.service")
self.system.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
self.system.wait_until_tty_matches("2", "login: ")
self.system.send_chars(f"{self.user}\n")
self.system.wait_until_tty_matches("2", f"login: {self.user}")
self.system.wait_until_succeeds("pgrep login")
self.system.sleep(2)
self.system.send_chars(f"{self.password}\n")
def setup(self) -> None:
"""Run common setup code."""
self.login()
def user_succeed(
self,
cmds: T,
directory: str | None = None,
debug: bool = False,
) -> T:
"""Run cmds as `self.user`, optionally in a specified directory.
For convenience, if cmds is a sequence, returns output as a list of
outputs corresponding with each line in cmds. if cmds is a string,
returns output as a string.
"""
context: list[str] = [
"set -Eeu -o pipefail",
"shopt -s inherit_errexit",
]
if debug:
context.append("set -x")
if directory:
context.append(f"cd {directory}")
if isinstance(cmds, str):
commands_str = "\n".join([*context, cmds])
final_command = f"sudo -u {self.user} -- bash -c '{commands_str}'"
return self.system.succeed(final_command)
results: list[str] = []
for cmd in cmds:
commands_str = "\n".join([*context, cmd])
final_command = f"sudo -u {self.user} -- bash -c '{commands_str}'"
result = self.system.succeed(final_command)
results.append(result.strip())
return t.cast(T, results)
def run_all(self) -> None:
self.test_rekeying()
self.test_user_edit()
def test_rekeying(self) -> None:
"""Ensure we can rekey a file and its hash changes."""
before_hash, _, after_hash = self.user_succeed(
[
"sha256sum passwordfile-user1.age",
f"agenix -r -i /home/{self.user}/.ssh/id_ed25519",
"sha256sum passwordfile-user1.age",
],
directory="/tmp/secrets",
)
# Ensure we actually have hashes
for line in [before_hash, after_hash]:
h = line.split()
assert len(h) == 2, f"hash should be [hash, filename], got {h}"
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"
def test_user_edit(self):
"""Ensure user1 can edit passwordfile-user1.age."""
self.user_succeed(
"EDITOR=cat agenix -e passwordfile-user1.age",
directory="/tmp/secrets",
)
self.user_succeed("echo bogus > ~/.ssh/id_rsa")
# Cannot edit with bogus default id_rsa
self.system.fail(
f"sudo -u {self.user} -- bash -c '"
"cd /tmp/secrets; "
"EDITOR=cat agenix -e /tmp/secrets/passwordfile-user1.age; "
"'"
)
# user1 can still edit if good identity specified
*_, pw = self.user_succeed(
[
(
"EDITOR=cat agenix -e passwordfile-user1.age "
"-i /home/user1/.ssh/id_ed25519"
),
"rm ~/.ssh/id_rsa",
"echo 'secret1234' | agenix -e passwordfile-user1.age",
"EDITOR=cat agenix -e passwordfile-user1.age",
],
directory="/tmp/secrets",
)
assert pw == "secret1234", f"password didn't match, got '{pw}'"

View file

@ -1,10 +1,17 @@
# Do not copy this! It is insecure. This is only okay because we are testing.
{
system.activationScripts.extraUserActivation.text = ''
echo "Installing SSH host key"
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
'';
}

View file

@ -6,27 +6,10 @@
config = {};
},
system ? builtins.currentSystem,
home-manager ? <home-manager>,
}:
pkgs.nixosTest {
name = "agenix-integration";
extraPythonPackages = ps: let
agenixTesting = let
version = (pkgs.callPackage ../pkgs/agenix.nix {}).version;
in
ps.buildPythonPackage rec {
inherit version;
pname = "agenix_testing";
src = ./.;
format = "pyproject";
propagatedBuildInputs = [ps.setuptools];
postPatch = ''
# Keep a default version makes for easy installation outside of
# nix for debugging
substituteInPlace pyproject.toml \
--replace 'version = "0.1.0"' 'version = "${version}"'
'';
};
in [agenixTesting];
nodes.system1 = {
config,
pkgs,
@ -36,6 +19,7 @@ pkgs.nixosTest {
imports = [
../modules/age.nix
./install_ssh_host_keys.nix
"${home-manager}/nixos"
];
services.openssh.enable = true;
@ -61,23 +45,82 @@ pkgs.nixosTest {
};
};
};
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 ''
# Skipping analyzing "agenix_testing": module is installed, but missing
# library stubs or py.typed marker
from agenix_testing import AgenixTester # type: ignore
tester = AgenixTester(system=system1, user="${user}", password="${password}")
# Can still be used as before
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")
# Or from `tester.system`
tester.system.wait_for_file("/tmp/1")
assert "${user}" in tester.system.succeed("cat /tmp/1")
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")
tester.run_all()
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

@ -8,7 +8,7 @@
testScript = pkgs.writeShellApplication {
name = "agenix-integration";
text = ''
grep ${secret} ${config.age.secrets.secret1.path}
grep "${secret}" "${config.age.secrets.system-secret.path}"
'';
};
in {
@ -19,9 +19,10 @@ in {
services.nix-daemon.enable = true;
age.identityPaths = options.age.identityPaths.default ++ ["/etc/ssh/this_key_wont_exist"];
age.secrets.secret1.file = ../example/secret1.age;
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')
'';
};
};
}

View file

@ -1,7 +0,0 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "agenix_testing"
version = "0.1.0"