diff --git a/example/secret1-copy.age b/example/secret1-copy.age new file mode 100644 index 0000000..8cb6f5e Binary files /dev/null and b/example/secret1-copy.age differ diff --git a/example/secrets.nix b/example/secrets.nix index 3bdac11..7b191f7 100644 --- a/example/secrets.nix +++ b/example/secrets.nix @@ -4,6 +4,7 @@ let in { "secret1.age".publicKeys = [ user1 system1 ]; + "secret1-copy.age".publicKeys = [ user1 system1 ]; "secret2.age".publicKeys = [ user1 ]; "passwordfile-user1.age".publicKeys = [ user1 system1 ]; } diff --git a/modules/age.nix b/modules/age.nix index 6bf53c6..d964897 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -18,10 +18,8 @@ let installSecret = secretType: '' ${if secretType.symlink then '' _truePath="${cfg.secretsMountPoint}/$_agenix_generation/${secretType.name}" - _oldPath="${cfg.secretsMountPoint}/$(( _agenix_generation - 1 ))/${secretType.name}" '' else '' _truePath="${secretType.path}" - _oldPath=_truePath ''} echo "decrypting '${secretType.file}' to '$_truePath'..." TMP_FILE="$_truePath.tmp" @@ -36,20 +34,37 @@ let chmod ${secretType.mode} "$TMP_FILE" chown ${secretType.owner}:${secretType.group} "$TMP_FILE" - # see if there's been any change from the last generation - changes=$(${pkgs.rsync}/bin/rsync --dry-run -aHAX -i "$TMP_FILE" "$_oldPath") + # path to the old version of the secret. cfg.secretsDir has already been + # updated to point at the latest generation, so we have to revert those + # paths. + outputPath="${cfg.secretsDir}/" + currentGenerationPath="${cfg.secretsMountPoint}/$_agenix_generation/" + previousGenerationPath="${cfg.secretsMountPoint}/$(( _agenix_generation - 1 ))/" + _oldPath=$(realpath ${secretType.path}) + _oldPath=''${_oldPath/#$currentGenerationPath/$previousGenerationPath} + _oldPath=''${_oldPath/#$outputPath/$previousGenerationPath} + [ -f $_oldPath ] && { + changes=$(${lib.concatStringsSep " " [ + "${pkgs.rsync}/bin/rsync" + "--dry-run -i" # just print what changed, don't do anything + "-aHAX" # care about everything (e.g. file permissions) + "--no-t -c" # but don't care about last modified timestamps + "$TMP_FILE" "$_oldPath" + ]}) + } || changes=true # _oldPath doesn't exist, so count it as a change mv -f "$TMP_FILE" "$_truePath" - [ "$changes" != "" ] && { - echo '${lib.concatStringsSep "\n" secretType.reloadUnits}' >> /run/nixos/activation-reload-list - echo '${lib.concatStringsSep "\n" secretType.restartUnits}' >> /run/nixos/activation-restart-list - ${secretType.onChange} - } - ${optionalString secretType.symlink '' [ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfn "${cfg.secretsDir}/${secretType.name}" "${secretType.path}" ''} + + # if /run/nixos doesn't exist, this is boot up and we don't need to activate the scripts. + [ "$changes" != "" ] && [ -d "/run/nixos" ] && { + echo '${lib.concatStringsSep "\n" secretType.reloadUnits}' >> /run/nixos/activation-reload-list + echo '${lib.concatStringsSep "\n" secretType.restartUnits}' >> /run/nixos/activation-restart-list + ${secretType.onChange} + } ''; testIdentities = map (path: '' diff --git a/test/integration.nix b/test/integration.nix index 8fb991e..17849fd 100644 --- a/test/integration.nix +++ b/test/integration.nix @@ -9,6 +9,29 @@ import "${nixpkgs}/nixos/tests/make-test-python.nix" enable = true; hostKeys = [{ type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }]; }; + + testService = name: { + systemd.services.${name} = { + wantedBy = [ "multi-user.target" ]; + reload = "touch /tmp/${name}-reloaded"; + # restarting a serivice stops it + preStop = "touch /tmp/${name}-stopped"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + }; + }; + + testSecret = name: { + imports = map testService [ "${name}-reloadUnit" "${name}-restartUnit" ]; + age.secrets.${name} = { + file = ../example/secret1.age; + onChange = "touch /tmp/${name}-onChange-executed"; + reloadUnits = [ "${name}-reloadUnit.service" ]; + restartUnits = [ "${name}-restartUnit.service" ]; + }; + }; in rec { name = "agenix-integration"; @@ -39,43 +62,43 @@ import "${nixpkgs}/nixos/tests/make-test-python.nix" imports = [ ../modules/age.nix ./install_ssh_host_keys.nix - ]; + ] + ++ map testSecret [ + "noChange" + "fileChange" + "secretChange" + "secretChangeWeirdPath" + "pathChange" + "pathChangeNoSymlink" + "modeChange" + "symlinkOn" + "symlinkOff" + ] + # add these services so they get started before the secret is added + ++ (testSecret "secretAdded").imports; + + + age.secrets.secretChangeWeirdPath.path = "/tmp/secretChangeWeirdPath"; + age.secrets.pathChangeNoSymlink.symlink = false; + age.secrets.symlinkOn.symlink = false; services.openssh = sshdConf; - - age.secrets.ex1 = { - file = ../example/passwordfile-user1.age; - onChange = "touch /tmp/onChange-executed"; - reloadUnits = [ "reloadTest.service" ]; - restartUnits = [ "restartTest.service" ]; - }; - - systemd.services.reloadTest = { - wantedBy = [ "multi-user.target" ]; - path = [ pkgs.coreutils ]; - reload = "touch /tmp/reloadTest-reloaded"; - preStop = "touch /tmp/reloadTest-stopped"; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - }; - - systemd.services.restartTest = { - wantedBy = [ "multi-user.target" ]; - path = [ pkgs.coreutils ]; - reload = "touch /tmp/restartTest-reloaded"; - preStop = "touch /tmp/restartTest-stopped"; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - }; }; nodes.system2After = { lib, ... }: { - imports = [ nodes.system2 ]; - age.secrets.ex1.file = lib.mkForce ../example/secret1.age; + imports = [ + nodes.system2 + # services have already been added + (builtins.removeAttrs (testSecret "secretAdded") [ "imports" ]) + ]; + age.secrets.fileChange.file = lib.mkForce ../example/secret1-copy.age; + age.secrets.secretChange.file = lib.mkForce ../example/passwordfile-user1.age; + age.secrets.secretChangeWeirdPath.file = lib.mkForce ../example/passwordfile-user1.age; + age.secrets.pathChange.path = lib.mkForce "/tmp/pathChange"; + age.secrets.pathChangeNoSymlink.path = lib.mkForce "/tmp/pathChangeNoSymlink"; + age.secrets.modeChange.mode = lib.mkForce "0777"; + age.secrets.symlinkOn.symlink = lib.mkForce true; + age.secrets.symlinkOff.symlink = lib.mkForce false; }; testScript = @@ -107,23 +130,52 @@ import "${nixpkgs}/nixos/tests/make-test-python.nix" # test changing secret system2.wait_for_unit("multi-user.target") - system2.wait_for_unit("reloadTest.service") - system2.wait_for_unit("restartTest.service") - # none of the files should exist yet. start blank - system2.fail("test -f /tmp/onChange-executed") - system2.fail("test -f /tmp/reloadTest-reloaded") - system2.fail("test -f /tmp/restartTest-stopped") - system2.fail("test -f /tmp/reloadTest-stopped") - # change the secret + # for these secrets the content doesn't change at all + system2_noChange_secrets = [ + "noChange", + "fileChange", + "symlinkOn", + "symlinkOff", + ] + system2_change_secrets = [ + "secretChange", + "secretChangeWeirdPath", + "pathChange", + "pathChangeNoSymlink", + "modeChange", + "secretAdded", + ] + system2_secrets = system2_noChange_secrets + system2_change_secrets + system2.wait_for_unit("multi-user.target") + for secret in system2_secrets: + system2.wait_for_unit(secret + "-reloadUnit") + system2.wait_for_unit(secret + "-restartUnit") + + def test_not_changed(secret): + system2.fail("test -f /tmp/" + secret + "-reloadUnit-reloaded") + system2.fail("test -f /tmp/" + secret + "-reloadUnit-restarted") + system2.fail("test -f /tmp/" + secret + "-restartUnit-reloaded") + system2.fail("test -f /tmp/" + secret + "-restartUnit-restarted") + system2.fail("test -f /tmp/" + secret + "-onChange-executed") + def test_changed(secret): + system2.wait_for_file("/tmp/" + secret + "-onChange-executed") + system2.wait_for_file("/tmp/" + secret + "-reloadUnit-reloaded") + system2.wait_for_file("/tmp/" + secret + "-restartUnit-stopped") + system2.fail("test -f /tmp/" + secret + "-reloadUnit-restarted") + system2.fail("test -f /tmp/" + secret + "-restartUnit-reloaded") + + # nothing should happen at startup + for secret in system2_secrets: + test_not_changed(secret) + + # apply changes system2.succeed( "${nodes.system2After.config.system.build.toplevel}/bin/switch-to-configuration test" ) - - system2.wait_for_file("/tmp/onChange-executed") - system2.wait_for_file("/tmp/reloadTest-reloaded") - system2.wait_for_file("/tmp/restartTest-stopped") - system2.fail("test -f /tmp/reloadTest-stopped") - system2.fail("test -f /tmp/restartTest-reloaded") + for secret in system2_noChange_secrets: + test_not_changed(secret) + for secret in system2_change_secrets: + test_changed(secret) ''; } )