From c1b9550d8e1e2b4633e6d25c4a9581570e55ae2d Mon Sep 17 00:00:00 2001 From: Andrew Lubawy Date: Wed, 24 Jul 2024 08:39:51 -0700 Subject: [PATCH 1/8] Try adding an option to output with armor --- pkgs/agenix.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkgs/agenix.sh b/pkgs/agenix.sh index 3d0415e..21446e6 100644 --- a/pkgs/agenix.sh +++ b/pkgs/agenix.sh @@ -118,6 +118,10 @@ function keys { (@nixInstantiate@ --json --eval --strict -E "(let rules = import $RULES; in rules.\"$1\".publicKeys)" | @jqBin@ -r .[]) || exit 1 } +function armor { + (@nixInstantiate@ --json --eval --strict -E "(let rules = import $RULES; in (builtins.hasAttr \"armor\" rules.\"$1\" && rules.\"$1\".armor))") || exit 1 +} + function decrypt { FILE=$1 KEYS=$2 @@ -148,6 +152,7 @@ function decrypt { function edit { FILE=$1 KEYS=$(keys "$FILE") || exit 1 + ARMOR=$(armor "$FILE") || exit 1 CLEARTEXT_DIR=$(@mktempBin@ -d) CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")" @@ -169,6 +174,9 @@ function edit { [ -f "$FILE" ] && [ "$EDITOR" != ":" ] && @diffBin@ -q "$CLEARTEXT_FILE.before" "$CLEARTEXT_FILE" && warn "$FILE wasn't changed, skipping re-encryption." && return ENCRYPT=() + if [[ "$ARMOR" == "true" ]]; then + ENCRYPT+=(--armor) + fi while IFS= read -r key do if [ -n "$key" ]; then From c14be7210ebe13b42682998b04b1b8589d378e7a Mon Sep 17 00:00:00 2001 From: Andrew Lubawy Date: Mon, 29 Jul 2024 10:28:45 -0700 Subject: [PATCH 2/8] Add armored example --- example/armored-secret.age | 7 +++++++ example/secrets.nix | 4 ++++ 2 files changed, 11 insertions(+) create mode 100644 example/armored-secret.age diff --git a/example/armored-secret.age b/example/armored-secret.age new file mode 100644 index 0000000..0e8bff1 --- /dev/null +++ b/example/armored-secret.age @@ -0,0 +1,7 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFYzWG1FQSBpZkZW +aFpLNnJxc0VUMHRmZ2dZS0pjMGVENnR3OHd5K0RiT1RjRUhibFZBCnN5UG5vUjA3 +SXpsNGtiVUw4T0tIVFo5Wkk5QS9NQlBndzVvektiQ0ozc0kKLS0tIGxyY1Q4dEZ1 +VGZEanJyTFNta2JNRmpZb2FnK2JyS1hSVml1UGdMNWZKQXMKYla+wTXcRedyZoEb +LVWaSx49WoUTU0KBPJg9RArxaeC23GoCDzR/aM/1DvYU +-----END AGE ENCRYPTED FILE----- diff --git a/example/secrets.nix b/example/secrets.nix index 2910329..2340a55 100644 --- a/example/secrets.nix +++ b/example/secrets.nix @@ -5,4 +5,8 @@ in { "secret1.age".publicKeys = [user1 system1]; "secret2.age".publicKeys = [user1]; "passwordfile-user1.age".publicKeys = [user1 system1]; + "armored-secret.age" = { + publicKeys = [ user1 ]; + armor = true; + }; } From af954310f1f6e649968d33fc3fed2688caa1bc46 Mon Sep 17 00:00:00 2001 From: Andrew Lubawy Date: Mon, 29 Jul 2024 10:44:12 -0700 Subject: [PATCH 3/8] Add integration test for armored secret --- test/integration.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/integration.nix b/test/integration.nix index e0ee85a..8479ac9 100644 --- a/test/integration.nix +++ b/test/integration.nix @@ -63,6 +63,10 @@ pkgs.nixosTest { file = ../example/secret2.age; path = "/home/user1/secret2"; }; + secrets.armored-secret = { + file = ../example/armored-secret.age; + path = "/home/user1/armored-secret"; + }; }; }; }; @@ -71,6 +75,7 @@ pkgs.nixosTest { user = "user1"; password = "password1234"; secret2 = "world!"; + armored-secret = "Hello World!"; in '' system1.wait_for_unit("multi-user.target") system1.wait_until_succeeds("pgrep -f 'agetty.*tty1'") @@ -89,8 +94,10 @@ pkgs.nixosTest { 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.send_chars("cat /run/user/$(id -u)/agenix/armored-secret > /tmp/3\n") system1.wait_for_file("/tmp/2") assert "${secret2}" in system1.succeed("cat /tmp/2") + assert "${armored-secret}" in system1.succeed("cat /tmp/3") userDo = lambda input : f"sudo -u user1 -- bash -c 'set -eou pipefail; cd /tmp/secrets; {input}'" From 7133e545ffdbaec51bd0db1791042bb71edd5594 Mon Sep 17 00:00:00 2001 From: Andrew Lubawy Date: Mon, 29 Jul 2024 10:50:01 -0700 Subject: [PATCH 4/8] Update docs to include example of armored output --- README.md | 20 ++++++++++++-------- doc/tutorial.md | 4 ++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 67718f6..58aaab7 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ `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: +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. @@ -250,7 +250,7 @@ e.g. inside your `flake.nix` file: $ cd secrets $ touch secrets.nix ``` - This `secrets.nix` file is **not** imported into your NixOS configuration. + 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 @@ -266,10 +266,14 @@ e.g. inside your `flake.nix` file: { "secret1.age".publicKeys = [ user1 system1 ]; "secret2.age".publicKeys = users ++ systems; + "armored-secret.age" = { + publicKeys = [ user1 ]; + armor = true; + }; } ``` 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 + 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 @@ -290,7 +294,7 @@ e.g. inside your `flake.nix` file: age.secrets.secret1.file = ../secrets/secret1.age; } ``` - 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`. + 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: @@ -306,14 +310,14 @@ e.g. inside your `flake.nix` file: 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. + 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 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 diff --git a/doc/tutorial.md b/doc/tutorial.md index 8344121..751afa9 100644 --- a/doc/tutorial.md +++ b/doc/tutorial.md @@ -25,6 +25,10 @@ { "secret1.age".publicKeys = [ user1 system1 ]; "secret2.age".publicKeys = users ++ systems; + "armored-secret.age" = { + publicKeys = [ user1 ]; + armor = true; + }; } ``` 4. Edit secret files (these instructions assume your SSH private key is in ~/.ssh/): From f6288a8fa745ba11db71059975b021ebd0f98c6a Mon Sep 17 00:00:00 2001 From: Andrew Lubawy Date: Tue, 30 Jul 2024 15:47:45 -0700 Subject: [PATCH 5/8] Run nix fmt on secrets.nix --- example/secrets.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/secrets.nix b/example/secrets.nix index 2340a55..84927f4 100644 --- a/example/secrets.nix +++ b/example/secrets.nix @@ -6,7 +6,7 @@ in { "secret2.age".publicKeys = [user1]; "passwordfile-user1.age".publicKeys = [user1 system1]; "armored-secret.age" = { - publicKeys = [ user1 ]; + publicKeys = [user1]; armor = true; }; } From acbc2e53b6abbabcf2b734fa2ebd9ade3838db4f Mon Sep 17 00:00:00 2001 From: Andrew Lubawy Date: Tue, 30 Jul 2024 15:55:25 -0700 Subject: [PATCH 6/8] Remove path config to use default secret path in test --- test/integration.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration.nix b/test/integration.nix index 8479ac9..09ce39a 100644 --- a/test/integration.nix +++ b/test/integration.nix @@ -65,7 +65,6 @@ pkgs.nixosTest { }; secrets.armored-secret = { file = ../example/armored-secret.age; - path = "/home/user1/armored-secret"; }; }; }; From a5725baf256d1d3dfd6b33d18c0dfcfd6c3a6c13 Mon Sep 17 00:00:00 2001 From: Andrew Lubawy Date: Tue, 30 Jul 2024 16:03:03 -0700 Subject: [PATCH 7/8] Add wait for file write before assertion --- test/integration.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration.nix b/test/integration.nix index 09ce39a..d2bfaaa 100644 --- a/test/integration.nix +++ b/test/integration.nix @@ -93,9 +93,10 @@ pkgs.nixosTest { 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.send_chars("cat /run/user/$(id -u)/agenix/armored-secret > /tmp/3\n") system1.wait_for_file("/tmp/2") assert "${secret2}" in system1.succeed("cat /tmp/2") + system1.send_chars("cat /run/user/$(id -u)/agenix/armored-secret > /tmp/3\n") + system1.wait_for_file("/tmp/3") assert "${armored-secret}" in system1.succeed("cat /tmp/3") userDo = lambda input : f"sudo -u user1 -- bash -c 'set -eou pipefail; cd /tmp/secrets; {input}'" From 0c9ed950df920298715d1305da70c1a5ce06257a Mon Sep 17 00:00:00 2001 From: Andrew Lubawy Date: Tue, 30 Jul 2024 16:44:03 -0700 Subject: [PATCH 8/8] Add more description on why armor may be useful --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 58aaab7..de5f8d5 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,7 @@ e.g. inside your `flake.nix` file: } ``` These are the users and systems that will be able to decrypt the `.age` files later with their corresponding private keys. + The armor option may also be supplied here to ensure files are output in Base64 PEM text which is useful for more readable diffs. 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`: