diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..125ac0c --- /dev/null +++ b/.github/release-drafter.yml @@ -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 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b53a3f2..e48411b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,27 +9,37 @@ jobs: - uses: actions/checkout@v3 - 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@v22 + - 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 - name: "Install nix-darwin module" run: | - system=$(nix build --no-link --print-out-paths .#checks.x86_64-darwin.integration) - ${system}/activate-user - sudo ${system}/activate + # 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 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..dd646d2 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -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 }} diff --git a/README.md b/README.md index 280a9b6..f96521a 100644 --- a/README.md +++ b/README.md @@ -45,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 @@ -205,7 +205,7 @@ You can run the CLI tool ad-hoc without installing it: nix run github:ryantm/agenix -- --help ``` -But you can also add it permanently into a [NixOS module](https://nixos.wiki/wiki/NixOS_modules) +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 @@ -273,7 +273,7 @@ e.g. inside your `flake.nix` file: * your local computer usually in `~/.ssh`, e.g. `~/.ssh/id_ed25519.pub`. * from a running target machine with `ssh-keyscan`: ```ShellSession - $ ssh-keyscan @ + $ ssh-keyscan ... ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1 ... ``` @@ -445,7 +445,7 @@ Example: #### `age.secrets..symlink` `age.secrets..symlink` is a boolean. If true (the default), -secrets are symlinked to `age.secrets..path`. If false, secerts +secrets are symlinked to `age.secrets..path`. If false, secrets are copied to `age.secrets..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 @@ -487,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: @@ -592,13 +592,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"; }) ]; } ``` @@ -623,6 +623,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 diff --git a/doc/notices.md b/doc/notices.md index 5dcc5a9..a186507 100644 --- a/doc/notices.md +++ b/doc/notices.md @@ -1,3 +1,3 @@ # Notices {#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. diff --git a/doc/overriding-age-binary.md b/doc/overriding-age-binary.md index 9ee3a11..34ae232 100644 --- a/doc/overriding-age-binary.md +++ b/doc/overriding-age-binary.md @@ -1,12 +1,12 @@ # Overriding age binary {#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"; }) ]; } ``` diff --git a/doc/reference.md b/doc/reference.md index 622c392..15db1c2 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -166,7 +166,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: diff --git a/flake.lock b/flake.lock index 3be370f..5d6236c 100644 --- a/flake.lock +++ b/flake.lock @@ -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": { @@ -28,11 +28,11 @@ ] }, "locked": { - "lastModified": 1682203081, - "narHash": "sha256-kRL4ejWDhi0zph/FpebFYhzqlOBrk0Pl3dzGEKSAlEw=", + "lastModified": 1703113217, + "narHash": "sha256-7ulcXOk63TIT2lVDSExj7XzFx09LpdSAPtvgtM7yQPE=", "owner": "nix-community", "repo": "home-manager", - "rev": "32d3e39c491e2f91152c84f8ad8b003420eab0a1", + "rev": "3bfaacf46133c037bb356193bd2f1765d9dc82c1", "type": "github" }, "original": { @@ -43,11 +43,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1677676435, - "narHash": "sha256-6FxdcmQr5JeZqsQvfinIMr0XcTyTuR7EXX0H3ANShpQ=", + "lastModified": 1703013332, + "narHash": "sha256-+tFNwMvlXLbJZXiMHqYq77z/RfmpfpiI3yjL6o/Zo9M=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a08d6979dd7c82c4cef0dcc6ac45ab16051c1169", + "rev": "54aac082a4d9bb5bbc5c4e899603abfb76a3f6d6", "type": "github" }, "original": { @@ -61,7 +61,23 @@ "inputs": { "darwin": "darwin", "home-manager": "home-manager", - "nixpkgs": "nixpkgs" + "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" } } }, diff --git a/flake.nix b/flake.nix index 13d863f..587138e 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,7 @@ url = "github:nix-community/home-manager"; inputs.nixpkgs.follows = "nixpkgs"; }; + systems.url = "github:nix-systems/default"; }; outputs = { @@ -18,9 +19,9 @@ nixpkgs, darwin, home-manager, + systems, }: let - agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {}; - doc = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/doc.nix {}; + eachSystem = nixpkgs.lib.genAttrs (import systems); in { nixosModules.age = import ./modules/age.nix; nixosModules.default = self.nixosModules.age; @@ -33,30 +34,13 @@ 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.doc = doc "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.doc = doc "aarch64-darwin"; - packages.aarch64-darwin.default = self.packages.aarch64-darwin.agenix; - - formatter.aarch64-linux = nixpkgs.legacyPackages.aarch64-linux.alejandra; - packages.aarch64-linux.agenix = agenix "aarch64-linux"; - packages.aarch64-linux.doc = doc "aarch64-linux"; - packages.aarch64-linux.default = self.packages.aarch64-linux.agenix; - - formatter.i686-linux = nixpkgs.legacyPackages.i686-linux.alejandra; - packages.i686-linux.agenix = agenix "i686-linux"; - packages.i686-linux.doc = doc "i686-linux"; - packages.i686-linux.default = self.packages.i686-linux.agenix; - - 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; - packages.x86_64-linux.doc = doc "x86_64-linux"; + packages = eachSystem (system: { + agenix = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {}; + doc = nixpkgs.legacyPackages.${system}.callPackage ./pkgs/doc.nix {inherit self;}; + default = self.packages.${system}.agenix; + }); checks = nixpkgs.lib.genAttrs ["aarch64-darwin" "x86_64-darwin"] (system: { @@ -65,7 +49,10 @@ inherit system; modules = [ ./test/integration_darwin.nix - "${darwin.outPath}/pkgs/darwin-installer/installer.nix" + + # Allow new-style nix commands in CI + {nix.extraOptions = "experimental-features = nix-command flakes";} + home-manager.darwinModules.home-manager { home-manager = { diff --git a/modules/age-home.nix b/modules/age-home.nix index 99ccccb..7c1051f 100644 --- a/modules/age-home.nix +++ b/modules/age-home.nix @@ -61,7 +61,7 @@ with lib; let ${optionalString secretType.symlink '' # shellcheck disable=SC2193,SC2050 - [ "${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}" ''} ''; @@ -76,7 +76,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 )))..." @@ -155,7 +155,7 @@ with lib; let ''; in { options.age = { - package = mkPackageOption pkgs "rage" {}; + package = mkPackageOption pkgs "age" {}; secrets = mkOption { type = types.attrsOf secretType; diff --git a/modules/age.nix b/modules/age.nix index 9468779..e49d9d8 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -69,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 @@ -87,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}" ''} ''; @@ -102,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 )))..." @@ -189,9 +190,9 @@ in { options.age = { ageBin = mkOption { type = types.str; - default = "${pkgs.rage}/bin/rage"; + default = "${pkgs.age}/bin/age"; defaultText = literalExpression '' - "''${pkgs.rage}/bin/rage" + "''${pkgs.age}/bin/age" ''; description = '' The age executable to use. diff --git a/pkgs/agenix.nix b/pkgs/agenix.nix index e399dd0..e51a5c6 100644 --- a/pkgs/agenix.nix +++ b/pkgs/agenix.nix @@ -1,37 +1,66 @@ { lib, stdenv, - rage, + age, jq, nix, mktemp, diffutils, substituteAll, - ageBin ? "${rage}/bin/rage", + ageBin ? "${age}/bin/age", shellcheck, -}: -stdenv.mkDerivation rec { - pname = "agenix"; - version = "0.14.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; +}: 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"; + } diff --git a/pkgs/agenix.sh b/pkgs/agenix.sh index 7ced252..c216563 100644 --- a/pkgs/agenix.sh +++ b/pkgs/agenix.sh @@ -148,7 +148,7 @@ function cleanup { trap "cleanup" 0 2 3 15 function keys { - (@nixInstantiate@ --json --eval --strict -E "(let rules = import $RULES; in rules.\"$FILE\".publicKeys)" | @jqBin@ -r .[]) || exit 1 + (@nixInstantiate@ --json --eval --strict -E "(let rules = import $RULES; in rules.\"$1\".publicKeys)" | @jqBin@ -r .[]) || exit 1 } function decrypt { @@ -188,7 +188,7 @@ function edit { decrypt "$FILE" "$KEYS" || exit 1 - cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before" + [ ! -f "$CLEARTEXT_FILE" ] || cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before" [ -t 0 ] || EDITOR='cp /dev/stdin' @@ -204,7 +204,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) @@ -214,7 +216,9 @@ function edit { @ageBin@ "${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1 - mv -f "$REENCRYPTED_FILE" "$1" + mkdir -p "$(dirname "$FILE")" + + mv -f "$REENCRYPTED_FILE" "$FILE" } function rekey {