oddlama / agenix-rekey

An agenix extension adding secret generation and automatic rekeying using a YubiKey or master-identity
MIT License
208 stars 17 forks source link

Selectively disabling copying generated secrets to host #43

Open dantefromhell opened 1 week ago

dantefromhell commented 1 week ago

My idea is to auto-generate and archive unique root passwords per machine using agenix-rekey.

I was able to setup some generators to get this to work using nicely.

age.rekey = {
  generatedSecretsDir = inputs.self.outPath + "/secrets/agenix/generated/${config.networking.fqdn}";
  localStorageDir = inputs.self.outPath + "/secrets/agenix/rekeyed/${config.networking.fqdn}";
  storageMode = "local";
};

random32 = { pkgs, ...  }: "''${pkgs.pwgen}/bin/pwgen --capitalize --numerals --symbols --secure --ambiguous 32 1";

hashedLinuxPassword = { decrypt, deps, lib, pkgs, ...}: let
  dep = builtins.head deps;
  in ''
    echo " -> Deriving yescrypt hash from "${lib.escapeShellArg dep.host}":"${lib.escapeShellArg dep.name}"" >&2
    ${decrypt} ${lib.escapeShellArg dep.file} \
      | tr -d '\n' \
      | ${pkgs.mkpasswd}/bin/mkpasswd --method=yescrypt --stdin \
      || die "Failure while generating yescrypt password hash"
  '';

age.secrets = {
  user-pw-root = { generator.script = "random32"; mode = "000"; };
  user-pw-root-hashed = {
    generator.dependencies = [ config.age.secrets.user-pw-root ];
    generator.script = "hashedLinuxPassword";
  };
};
users.users.root.hashedPasswordFile = config.age.secrets.user-pw-root-hashed.path;

With this setup the unencrypted root password will be stored in /run/agenix.d/1/user-pw-root which is not ideal from a security perspective and superfluous since the hash is also available.

It would be great to have an option to prevent user-pw-root from being copied to the machine, maybe something like:

user-pw-root = { generator.script = "random32"; copy=false; };
oddlama commented 1 week ago

This is a pretty great idea, I've would have had use for this myself already, but it requires changes to agenix to work properly since agenix installs anything in config.age.secrets without verifying any conditions beforehand.

So our only way to achieve this right now would be to replace the age.secrets.<name>.file with an empty dummy secret. This is kind of hacky but I guess would be an OK workaround for now. I'm not sure on the naming of the option yet, maybe we should call it intermediary or something like that to communicate that this is a secret not associated to the host at all.

dantefromhell commented 1 week ago

I'm not sure on the naming of the option yet

Yeah I struggled with that one too - my suggestion above was the best I could come up with in the moment.

something like that to communicate that this is a secret not associated to the host at all

Yeah that was my though too.

only way to achieve this right now would be to replace the age.secrets..file with an empty dummy secret

I'm not sure I get this right, would you be able to provide an example?

oddlama commented 1 week ago

I'm not sure I get this right, would you be able to provide an example?

When you define a secret, you set age.secrets.mysecret.rekeyFile = ./something.age; and maybe some other attributes. agenix-rekey then automatically set age.secrets.mysecret.file = ./something-but-rekeyed-for-the-host.age. The file option is mandatory and must be set, since agenix itself decrypts ALL secrets in age.secets when the host's activation script runs. There is no way to tell agenix to ignore one of these entries.

So to have an option age.secrets.mysecret.copy = false; that works, it would have to remove itself (mysecret) from age.secrets, because agenix will try to decrypt anything in that attrset. But you cannot undefine an option while also using it's own value to say it should be removed (would cause infinite recursion). So to introduce a copy option we need something that is either respected by agenix directly (which then would require upstream changes to agenix) or we can use a fake file = ./dummy.age that can be decrypted on the host but doesn't actually contain the secret.