Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ when `pure-eval = true` is set in `nix.conf`. This allows it to read the selecte
file and any local files imported by those rules.

Select one operation per invocation: edit, decrypt, rekey, or check. Each command
evaluates the rules it needs once. Editing or decrypting a single file does not
evaluates the rules it needs once. Editing, decrypting, or rekeying a single file does not
evaluate unrelated secrets, and checking or decrypting does not evaluate armor
settings. Secret filenames are literal rule names and may contain spaces or
quotes. Relative filenames resolve from the selected rules file's directory.
Expand All @@ -348,12 +348,14 @@ agenix - edit, rekey, and check age secret files

agenix -e FILE [-i PRIVATE_KEY] [-j PLUGIN]
agenix -r [PUBLIC_KEY] [-i PRIVATE_KEY] [-j PLUGIN]
agenix --rekey-file FILE [-i PRIVATE_KEY] [-j PLUGIN]
agenix -c

options:
-h, --help show help
-e, --edit FILE edits FILE using $EDITOR
-r, --rekey [PUBLIC_KEY] re-encrypts secrets, optionally selecting a recipient
--rekey-file FILE re-encrypts only FILE with its current recipients
-c, --check checks encrypted SSH recipients against the rules
-d, --decrypt FILE decrypts FILE to STDOUT
-i, --identity identity to use when decrypting
Expand Down
11 changes: 11 additions & 0 deletions doc/rekeying.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ secrets:
$ agenix --rekey
```

To rekey one file after changing its recipient list:

```ShellSession
$ agenix --rekey-file secrets/database.age -i ~/.ssh/id_ed25519
```

The filename is relative to the selected rules file's directory and must match
a rule. Only that rule is evaluated and only that file is re-encrypted. The
command requires an identity that can decrypt the existing file, does not open
an editor, and ignores piped input. Quote filenames containing spaces.

To rekey only secrets whose current rules include a particular public key,
pass the key as a quoted argument:

Expand Down
13 changes: 11 additions & 2 deletions pkgs/agenix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ function show_help () {
echo " "
echo "${PACKAGE} -e FILE [-i PRIVATE_KEY] [-j PLUGIN]"
echo "${PACKAGE} -r [PUBLIC_KEY] [-i PRIVATE_KEY] [-j PLUGIN]"
echo "${PACKAGE} --rekey-file FILE [-i PRIVATE_KEY] [-j PLUGIN]"
echo "${PACKAGE} -c"
echo ' '
echo 'options:'
echo '-h, --help show help'
echo "-e, --edit FILE edits FILE using \$EDITOR"
echo '-r, --rekey [PUBLIC_KEY] re-encrypts secrets, optionally selecting a recipient'
echo '--rekey-file FILE re-encrypts only FILE with its current recipients'
echo '-c, --check checks encrypted SSH recipients against the rules'
echo '-d, --decrypt FILE decrypts FILE to STDOUT'
echo '-i, --identity identity to use when decrypting'
Expand Down Expand Up @@ -61,7 +63,7 @@ function set_file() {
}

function set_operation() {
[[ -z ${OPERATION} ]] || err 'Select only one of --edit, --decrypt, --rekey, or --check.'
[[ -z ${OPERATION} ]] || err 'Select only one of --edit, --decrypt, --rekey, --rekey-file, or --check.'
OPERATION=$1
}

Expand Down Expand Up @@ -121,6 +123,13 @@ while test $# -gt 0; do
shift
fi
;;
--rekey-file)
set_operation rekey
shift
[[ $# -gt 0 ]] || err 'no FILE specified'
set_file "$1"
shift
;;
-c|--check)
set_operation check
shift
Expand Down Expand Up @@ -228,7 +237,7 @@ RULE_DATA=$(@nixInstantiate@ --impure --json --eval --strict \
-E '{ rulesPath, operation, file, recipient }:
let
rules = import (builtins.toPath rulesPath);
names = if operation == "edit" || operation == "decrypt"
names = if file != ""
then [ file ] else builtins.attrNames rules;
selected = builtins.filter
(name: recipient == "" || builtins.elem recipient (builtins.getAttr name rules).publicKeys)
Expand Down
1 change: 1 addition & 0 deletions test/cli-plugin.nix
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pkgs.runCommand "agenix-cli-plugin" { nativeBuildInputs = [ cli ]; } ''
test "$(agenix -d secret1.age -j fixture)" = hello
EDITOR=: agenix -e secret1.age -j fixture
agenix -r -j fixture
agenix --rekey-file secret1.age -j fixture
test "$(agenix -d secret1.age -j fixture)" = hello
if agenix -d secret1.age -j unknown; then exit 1; fi
for args in '-j' '-j -r'; do
Expand Down
16 changes: 16 additions & 0 deletions test/cli-rules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ EOF
once "$agenix" -d "$strange" -i "$identity" > decrypted
printf replacement | once "$agenix" -e "$strange"
test "$("$agenix" -d "$strange" -i "$identity")" = replacement
cp -- "$strange" selected.age
cp -- "$plain" unselected.age
printf 'must not replace plaintext' | EDITOR=false once "$agenix" --rekey-file "$strange" -i "$identity"
! cmp -- selected.age "$strange"
cmp -- unselected.age "$plain"
test "$("$agenix" -d "$strange" -i "$identity")" = replacement
cp -- "$strange" selected.age
if "$agenix" --rekey-file "$strange" > error 2>&1; then exit 1; fi
cmp -- selected.age "$strange"
test -z "$(ls -A "$TMPDIR")"
cat > "$AGENIX_RULES" <<'EOF'
builtins.mapAttrs (_: rule: rule // {
armor = throw "unused armor forced";
Expand Down Expand Up @@ -80,6 +90,9 @@ for operation in -d -e -c -r; do
done
if "$agenix" > error 2>&1; then exit 1; fi
if "$agenix" -e '' > error 2>&1; then exit 1; fi
if "$agenix" --rekey-file > error 2>&1; then exit 1; fi
grep -q 'no FILE specified' error
if "$agenix" --rekey-file '' > error 2>&1; then exit 1; fi

# A failing editor may modify plaintext, but must not replace the encrypted file.
cat > "$TMPDIR/../failed-editor" <<'EOF'
Expand Down Expand Up @@ -127,6 +140,9 @@ if "$agenix" -r -i "$identity" > error 2>&1; then exit 1; fi
grep -q 'does not exist' error
test ! -e "$plain"
test -z "$(ls -A "$TMPDIR")"
if "$agenix" --rekey-file "$plain" -i "$identity" > error 2>&1; then exit 1; fi
grep -q 'does not exist' error
test ! -e "$plain"
if "$agenix" -d "$plain" -i "$identity" > error 2>&1; then exit 1; fi
grep -q 'does not exist' error

Expand Down
6 changes: 6 additions & 0 deletions test/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ before=$(hash passwordfile-user1.age)
"$agenix" -r -i "$HOME/.ssh/id_ed25519"
[[ $(hash passwordfile-user1.age) != "$before" ]]

selected_before=$(hash ./-leading-hyphen-filename.age)
unselected_before=$(hash secret1.age)
"$agenix" --rekey-file ./-leading-hyphen-filename.age -i "$HOME/.ssh/id_ed25519"
[[ $(hash ./-leading-hyphen-filename.age) != "$selected_before" ]]
[[ $(hash secret1.age) == "$unselected_before" ]]

# A recipient filter selects by the current rules and keeps other files intact.
selected_before=$(hash secret1.age)
unselected_before=$(hash secret2.age)
Expand Down
Loading