diff --git a/modules/age.nix b/modules/age.nix index a4631f7..1b8d0bf 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -1,14 +1,17 @@ -{ config, options, lib, pkgs, ... }: - -with lib; - -let +{ + config, + options, + lib, + pkgs, + ... +}: +with lib; let cfg = config.age; # 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 { } + then pkgs.callPackage ../pkgs/rage.nix {} else pkgs.rage; ageBin = config.age.ageBin; @@ -28,11 +31,15 @@ let identities = builtins.concatStringsSep " " (map (path: "-i ${path}") cfg.identityPaths); setTruePath = secretType: '' - ${if secretType.symlink then '' - _truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}" - '' else '' - _truePath="${secretType.path}" - ''} + ${ + if secretType.symlink + then '' + _truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}" + '' + else '' + _truePath="${secretType.path}" + '' + } ''; installSecret = secretType: '' @@ -55,9 +62,11 @@ let ''} ''; - testIdentities = map (path: '' - test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!' - '') cfg.identityPaths; + 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)" @@ -72,10 +81,10 @@ let ''; installSecrets = builtins.concatStringsSep "\n" ( - [ "echo '[agenix] decrypting secrets...'" ] + ["echo '[agenix] decrypting secrets...'"] ++ testIdentities ++ (map installSecret (builtins.attrValues cfg.secrets)) - ++ [ cleanupAndLink ] + ++ [cleanupAndLink] ); chownSecret = secretType: '' @@ -90,11 +99,12 @@ let ''; chownSecrets = builtins.concatStringsSep "\n" ( - [ "echo '[agenix] chowning...'" ] - ++ [ chownMountPoint ] - ++ (map chownSecret (builtins.attrValues cfg.secrets))); + ["echo '[agenix] chowning...'"] + ++ [chownMountPoint] + ++ (map chownSecret (builtins.attrValues cfg.secrets)) + ); - secretType = types.submodule ({ config, ... }: { + secretType = types.submodule ({config, ...}: { options = { name = mkOption { type = types.str; @@ -137,14 +147,12 @@ let Group of the decrypted secret. ''; }; - symlink = mkEnableOption "symlinking secrets to their destination" // { default = true; }; + symlink = mkEnableOption "symlinking secrets to their destination" // {default = true;}; }; }); -in -{ - +in { imports = [ - (mkRenamedOptionModule [ "age" "sshKeyPaths" ] [ "age" "identityPaths" ]) + (mkRenamedOptionModule ["age" "sshKeyPaths"] ["age" "identityPaths"]) ]; options.age = { @@ -157,7 +165,7 @@ in }; secrets = mkOption { type = types.attrsOf secretType; - default = { }; + default = {}; description = '' Attrset of secrets. ''; @@ -170,11 +178,13 @@ in ''; }; secretsMountPoint = mkOption { - type = types.addCheck types.str + type = + types.addCheck types.str (s: - (builtins.match "[ \t\n]*" s) == null # non-empty - && (builtins.match ".+/" s) == null) # without trailing slash - // { description = "${types.str.description} (with check: non-empty without trailing slash)"; }; + (builtins.match "[ \t\n]*" s) + == null # non-empty + && (builtins.match ".+/" s) == null) # without trailing slash + // {description = "${types.str.description} (with check: non-empty without trailing slash)";}; default = "/run/agenix.d"; defaultText = "/run/agenix.d"; description = '' @@ -184,20 +194,22 @@ in identityPaths = mkOption { type = types.listOf types.path; default = - if config.services.openssh.enable then - map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys) - else [ ]; + if config.services.openssh.enable + then map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys) + else []; description = '' Path to SSH keys to be used as identities in age decryption. ''; }; }; - config = mkIf (cfg.secrets != { }) { - assertions = [{ - assertion = cfg.identityPaths != [ ]; - message = "age.identityPaths must be set."; - }]; + config = mkIf (cfg.secrets != {}) { + assertions = [ + { + assertion = cfg.identityPaths != []; + message = "age.identityPaths must be set."; + } + ]; # Create a new directory full of secrets for symlinking (this helps # ensure removed secrets are actually removed, or at least become @@ -218,7 +230,7 @@ in }; # So user passwords can be encrypted. - system.activationScripts.users.deps = [ "agenixInstall" ]; + system.activationScripts.users.deps = ["agenixInstall"]; # Change ownership and group after users and groups are made. system.activationScripts.agenixChown = { @@ -232,8 +244,7 @@ in # So other activation scripts can depend on agenix being done. system.activationScripts.agenix = { text = ""; - deps = [ "agenixChown"]; + deps = ["agenixChown"]; }; }; - } diff --git a/overlay.nix b/overlay.nix index de0e8a6..f484e44 100644 --- a/overlay.nix +++ b/overlay.nix @@ -1,4 +1,3 @@ -final: prev: -{ - agenix = prev.callPackage ./pkgs/agenix.nix { }; +final: prev: { + agenix = prev.callPackage ./pkgs/agenix.nix {}; } diff --git a/pkgs/agenix.nix b/pkgs/agenix.nix index e62861a..56e3da3 100644 --- a/pkgs/agenix.nix +++ b/pkgs/agenix.nix @@ -13,177 +13,175 @@ if rage.version < "0.5.0" then callPackage ./rage.nix {} else rage - }/bin/rage" -} : -let + }/bin/rage", +}: let sedBin = "${gnused}/bin/sed"; nixInstantiate = "${nix}/bin/nix-instantiate"; mktempBin = "${mktemp}/bin/mktemp"; diffBin = "${diffutils}/bin/diff"; in -lib.recursiveUpdate (writeShellScriptBin "agenix" '' -set -Eeuo pipefail + lib.recursiveUpdate (writeShellScriptBin "agenix" '' + set -Eeuo pipefail -PACKAGE="agenix" + PACKAGE="agenix" -function show_help () { - echo "$PACKAGE - edit and rekey age secret files" - echo " " - echo "$PACKAGE -e FILE [-i PRIVATE_KEY]" - echo "$PACKAGE -r [-i PRIVATE_KEY]" - echo ' ' - echo 'options:' - echo '-h, --help show help' - echo '-e, --edit FILE edits FILE using $EDITOR' - echo '-r, --rekey re-encrypts all secrets with specified recipients' - echo '-i, --identity identity to use when decrypting' - echo '-v, --verbose verbose output' - echo ' ' - echo 'FILE an age-encrypted file' - echo ' ' - echo 'PRIVATE_KEY a path to a private SSH key used to decrypt file' - echo ' ' - echo 'EDITOR environment variable of editor to use when editing FILE' - echo ' ' - echo 'RULES environment variable with path to Nix file specifying recipient public keys.' - echo "Defaults to './secrets.nix'" - echo ' ' - echo "agenix version: 0.13.0" - echo "age binary path: ${ageBin}" - echo "age version: $(${ageBin} --version)" -} + function show_help () { + echo "$PACKAGE - edit and rekey age secret files" + echo " " + echo "$PACKAGE -e FILE [-i PRIVATE_KEY]" + echo "$PACKAGE -r [-i PRIVATE_KEY]" + echo ' ' + echo 'options:' + echo '-h, --help show help' + echo '-e, --edit FILE edits FILE using $EDITOR' + echo '-r, --rekey re-encrypts all secrets with specified recipients' + echo '-i, --identity identity to use when decrypting' + echo '-v, --verbose verbose output' + echo ' ' + echo 'FILE an age-encrypted file' + echo ' ' + echo 'PRIVATE_KEY a path to a private SSH key used to decrypt file' + echo ' ' + echo 'EDITOR environment variable of editor to use when editing FILE' + echo ' ' + echo 'RULES environment variable with path to Nix file specifying recipient public keys.' + echo "Defaults to './secrets.nix'" + echo ' ' + echo "agenix version: 0.13.0" + echo "age binary path: ${ageBin}" + echo "age version: $(${ageBin} --version)" + } -test $# -eq 0 && (show_help && exit 1) + test $# -eq 0 && (show_help && exit 1) -REKEY=0 -DEFAULT_DECRYPT=(--decrypt) + REKEY=0 + DEFAULT_DECRYPT=(--decrypt) -while test $# -gt 0; do - case "$1" in - -h|--help) - show_help - exit 0 - ;; - -e|--edit) - shift - if test $# -gt 0; then - export FILE=$1 - else - echo "no FILE specified" - exit 1 - fi - shift - ;; - -i|--identity) - shift - if test $# -gt 0; then - DEFAULT_DECRYPT+=(--identity "$1") - else - echo "no PRIVATE_KEY specified" - exit 1 - fi - shift - ;; - -r|--rekey) - shift - REKEY=1 - ;; - -v|--verbose) - shift - set -x - ;; - *) - show_help - exit 1 - ;; - esac -done - -RULES=''${RULES:-./secrets.nix} - -function cleanup { - if [ ! -z ''${CLEARTEXT_DIR+x} ] - then - rm -rf "$CLEARTEXT_DIR" - fi - if [ ! -z ''${REENCRYPTED_DIR+x} ] - then - rm -rf "$REENCRYPTED_DIR" - fi -} -trap "cleanup" 0 2 3 15 - -function edit { - FILE=$1 - KEYS=$((${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) - - if [ -z "$KEYS" ] - then - >&2 echo "There is no rule for $FILE in $RULES." - exit 1 - fi - - CLEARTEXT_DIR=$(${mktempBin} -d) - CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")" - - if [ -f "$FILE" ] - then - DECRYPT=("''${DEFAULT_DECRYPT[@]}") - if [ -f "$HOME/.ssh/id_rsa" ]; then - DECRYPT+=(--identity "$HOME/.ssh/id_rsa") - fi - if [ -f "$HOME/.ssh/id_ed25519" ]; then - DECRYPT+=(--identity "$HOME/.ssh/id_ed25519") - fi - if [[ "''${DECRYPT[*]}" != *"--identity"* ]]; then - echo "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." + while test $# -gt 0; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + -e|--edit) + shift + if test $# -gt 0; then + export FILE=$1 + else + echo "no FILE specified" + exit 1 + fi + shift + ;; + -i|--identity) + shift + if test $# -gt 0; then + DEFAULT_DECRYPT+=(--identity "$1") + else + echo "no PRIVATE_KEY specified" + exit 1 + fi + shift + ;; + -r|--rekey) + shift + REKEY=1 + ;; + -v|--verbose) + shift + set -x + ;; + *) + show_help exit 1 - fi - DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE") - ${ageBin} "''${DECRYPT[@]}" || exit 1 - cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before" - fi - - $EDITOR "$CLEARTEXT_FILE" - - if [ ! -f "$CLEARTEXT_FILE" ] - then - echo "$FILE wasn't created." - return - fi - [ -f "$FILE" ] && [ "$EDITOR" != ":" ] && ${diffBin} "$CLEARTEXT_FILE.before" "$CLEARTEXT_FILE" 1>/dev/null && echo "$FILE wasn't changed, skipping re-encryption." && return - - ENCRYPT=() - while IFS= read -r key - do - ENCRYPT+=(--recipient "$key") - done <<< "$KEYS" - - REENCRYPTED_DIR=$(${mktempBin} -d) - REENCRYPTED_FILE="$REENCRYPTED_DIR/$(basename "$FILE")" - - ENCRYPT+=(-o "$REENCRYPTED_FILE") - - ${ageBin} "''${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1 - - mv -f "$REENCRYPTED_FILE" "$1" -} - -function rekey { - FILES=$((${nixInstantiate} --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" (builtins.attrNames rules))" | ${sedBin} 's/"//g' | ${sedBin} 's/\\n/\n/g') || exit 1) - - for FILE in $FILES - do - echo "rekeying $FILE..." - EDITOR=: edit "$FILE" - cleanup + ;; + esac done -} -[ $REKEY -eq 1 ] && rekey && exit 0 -edit "$FILE" && cleanup && exit 0 -'') + RULES=''${RULES:-./secrets.nix} -{ - meta.description = "age-encrypted secrets for NixOS"; -} + function cleanup { + if [ ! -z ''${CLEARTEXT_DIR+x} ] + then + rm -rf "$CLEARTEXT_DIR" + fi + if [ ! -z ''${REENCRYPTED_DIR+x} ] + then + rm -rf "$REENCRYPTED_DIR" + fi + } + trap "cleanup" 0 2 3 15 + + function edit { + FILE=$1 + KEYS=$((${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) + + if [ -z "$KEYS" ] + then + >&2 echo "There is no rule for $FILE in $RULES." + exit 1 + fi + + CLEARTEXT_DIR=$(${mktempBin} -d) + CLEARTEXT_FILE="$CLEARTEXT_DIR/$(basename "$FILE")" + + if [ -f "$FILE" ] + then + DECRYPT=("''${DEFAULT_DECRYPT[@]}") + if [ -f "$HOME/.ssh/id_rsa" ]; then + DECRYPT+=(--identity "$HOME/.ssh/id_rsa") + fi + if [ -f "$HOME/.ssh/id_ed25519" ]; then + DECRYPT+=(--identity "$HOME/.ssh/id_ed25519") + fi + if [[ "''${DECRYPT[*]}" != *"--identity"* ]]; then + echo "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." + exit 1 + fi + DECRYPT+=(-o "$CLEARTEXT_FILE" "$FILE") + ${ageBin} "''${DECRYPT[@]}" || exit 1 + cp "$CLEARTEXT_FILE" "$CLEARTEXT_FILE.before" + fi + + $EDITOR "$CLEARTEXT_FILE" + + if [ ! -f "$CLEARTEXT_FILE" ] + then + echo "$FILE wasn't created." + return + fi + [ -f "$FILE" ] && [ "$EDITOR" != ":" ] && ${diffBin} "$CLEARTEXT_FILE.before" "$CLEARTEXT_FILE" 1>/dev/null && echo "$FILE wasn't changed, skipping re-encryption." && return + + ENCRYPT=() + while IFS= read -r key + do + ENCRYPT+=(--recipient "$key") + done <<< "$KEYS" + + REENCRYPTED_DIR=$(${mktempBin} -d) + REENCRYPTED_FILE="$REENCRYPTED_DIR/$(basename "$FILE")" + + ENCRYPT+=(-o "$REENCRYPTED_FILE") + + ${ageBin} "''${ENCRYPT[@]}" <"$CLEARTEXT_FILE" || exit 1 + + mv -f "$REENCRYPTED_FILE" "$1" + } + + function rekey { + FILES=$((${nixInstantiate} --eval -E "(let rules = import $RULES; in builtins.concatStringsSep \"\n\" (builtins.attrNames rules))" | ${sedBin} 's/"//g' | ${sedBin} 's/\\n/\n/g') || exit 1) + + for FILE in $FILES + do + echo "rekeying $FILE..." + EDITOR=: edit "$FILE" + cleanup + done + } + + [ $REKEY -eq 1 ] && rekey && exit 0 + edit "$FILE" && cleanup && exit 0 + '') + { + meta.description = "age-encrypted secrets for NixOS"; + } diff --git a/pkgs/rage.nix b/pkgs/rage.nix index 8db697d..4db11e9 100644 --- a/pkgs/rage.nix +++ b/pkgs/rage.nix @@ -1,5 +1,10 @@ -{ stdenv, rustPlatform, fetchFromGitHub, installShellFiles, darwin }: - +{ + stdenv, + rustPlatform, + fetchFromGitHub, + installShellFiles, + darwin, +}: rustPlatform.buildRustPackage rec { pname = "rage"; version = "0.5.0"; @@ -13,12 +18,13 @@ rustPlatform.buildRustPackage rec { cargoSha256 = "sha256-GPr5zxeODAjD+ynp/nned9gZUiReYcdzosuEbLIKZSs="; - nativeBuildInputs = [ installShellFiles ]; + nativeBuildInputs = [installShellFiles]; - buildInputs = with darwin.apple_sdk.frameworks; stdenv.lib.optionals stdenv.isDarwin [ - Security - Foundation - ]; + buildInputs = with darwin.apple_sdk.frameworks; + stdenv.lib.optionals stdenv.isDarwin [ + Security + Foundation + ]; # cargo test has an x86-only dependency doCheck = stdenv.hostPlatform.isx86; @@ -37,7 +43,7 @@ rustPlatform.buildRustPackage rec { 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 ]; + license = with licenses; [asl20 mit]; # either at your option + maintainers = with maintainers; [marsam ryantm]; }; } diff --git a/test/install_ssh_host_keys.nix b/test/install_ssh_host_keys.nix index 93f383d..b431173 100644 --- a/test/install_ssh_host_keys.nix +++ b/test/install_ssh_host_keys.nix @@ -1,6 +1,6 @@ # Do not copy this! It is insecure. This is only okay because we are testing. { - system.activationScripts.agenixInstall.deps = [ "installSSHHostKeys" ]; + system.activationScripts.agenixInstall.deps = ["installSSHHostKeys"]; system.activationScripts.installSSHHostKeys.text = '' mkdir -p /etc/ssh diff --git a/test/integration.nix b/test/integration.nix index 90f8b8b..01d8e3a 100644 --- a/test/integration.nix +++ b/test/integration.nix @@ -1,14 +1,20 @@ { -nixpkgs ? , -pkgs ? import { inherit system; config = {}; }, -system ? builtins.currentSystem -} @args: - -import "${nixpkgs}/nixos/tests/make-test-python.nix" ({ pkgs, ...}: { + nixpkgs ? , + pkgs ? + import { + inherit system; + config = {}; + }, + system ? builtins.currentSystem, +} @ args: +import "${nixpkgs}/nixos/tests/make-test-python.nix" ({pkgs, ...}: { name = "agenix-integration"; - nodes.system1 = { config, lib, ... }: { - + nodes.system1 = { + config, + lib, + ... + }: { imports = [ ../modules/age.nix ./install_ssh_host_keys.nix @@ -30,11 +36,9 @@ import "${nixpkgs}/nixos/tests/make-test-python.nix" ({ pkgs, ...}: { }; }; }; - }; - testScript = - let + testScript = let user = "user1"; password = "password1234"; in '' @@ -55,4 +59,5 @@ import "${nixpkgs}/nixos/tests/make-test-python.nix" ({ pkgs, ...}: { system1.wait_for_file("/tmp/1") assert "${user}" in system1.succeed("cat /tmp/1") ''; -}) args +}) +args