Closed axelkar closed 7 months ago
Oh and I'd like to ask on how to do the specific secrets for specific hosts thing.
Technically yes, but it depends on whether you are comfortable using this. Manually generating the private and public key pair is always an option, and you might consider that "cleaner" than what I'm going to propose now.
Since agenix rekey
is run separately and generates files before you actually build the system, you can make it accessible at build time. The issue is that by using agenix-rekey you will end up with encrypted files, which cannot be imported easily at build-time.
Option A is quite hacky, but by using nix-plugins you can define a new builtin that allows you to import encrypted files at build time. This is what I am doing in my personal repository to store PII, and this could read the host pubkey which you can derive via agenix-rekey. See below for a full example.
Option B would be to use a generator that automatically places the public key alongside the rekeyed file.
age.secrets.ssh-private-key = {
rekeyFile = ./hosts1/secrets/id_ed25519;
generator.script = { pkgs, file, ... }: ''
privkey=$( (exec 3>&1; ${pkgs.openssh}/bin/ssh-keygen -q -t ed25519 -N "" -f /proc/self/fd/3 <<<y >/dev/null 2>&1; true) )
# Derive public key from private key
echo "$privkey" | (exec 3<&0; ssh-keygen -f /proc/self/fd/3 -y) >${lib.escapeShellArg file}.pub
# Only output private key for this secret
echo "$privkey"
'';
};
Now you can just refer to ./host1/secrets/id_ed25519.pub
from anywhere.
Assuming you want to use nix-plugins, you can let agenix-rekey derive the public key via a dependency and let it be stored in an encrypted format. Then you will import the file using a custom builtin. Not recommended, but works :)
There already is a builtin generator for private keys, and then you can derive the public key from that in a second secret. For this to work properly, you need to have access to the other nodes configuration via your specialArgs
, so whatever you set for agenix-rekey.nodes
in your flake.nix
should also be accessible in your host's configuration.nix in case you don't have that already. Then you can make the public key's secret depend on the other host's private key secret so it will automatically be generated in case the secret key changes.
So assuming you have access to your other nodes via nodes.<name>
in your nixos configuration, you can do the following:
host1's configuration.nix
(the host using the private key):
{
age.secrets.ssh-private-key = {
rekeyFile = ./secrets/id_ed25519;
generator.script = "ssh-ed25519";
};
}
host2's configuration.nix
(derives and uses the public key):
{ nodes, ... }: {
age.secrets.host1-ssh-public-key = {
rekeyFile = ./secrets/host1_id_ed25519.pub;
generator.dependencies = [
nodes.host1.config.age.secrets.ssh-private-key
];
generator.script = { lib, decrypt, deps, ... }: ''
# Derive public key from private key
${decrypt} ${lib.escapeShellArg (lib.head deps).file} | (exec 3<&0; ssh-keygen -f /proc/self/fd/3 -y)
'';
};
}
Sorry I misread your question initially. My answer is now updated :)
>${lib.escapeShellArg file}.pub
I'm a little worried Nix would just make file
a store path but I haven't looked at that part of the library's source code yet so maybe you have something to kill the string context or make rekeyFile a string when generating.
./secrets/id_ed25519
,./secrets/host1_id_ed25519.pub
Is there a reason the file paths don't end in .age
?
Thank you for the help!
Btw is the rekeyed secret derivation hash really different depending on the target hosts's system?
https://github.com/oddlama/agenix-rekey/blob/main/nix/output-derivation.nix#L68
Why don't you just make a tempdir in the agenix rekey
command wherever and let Nix copy the path (yes Nix path type)?
# pseudo bash code for `agenix rekey`
originalSecret=${config.age.secrets.foo.rekeyFile passed in from nix}
originalSecretHash="$(sha256sum $originalSecret | cut -d " " -f 1)"
decrypt $originalSecret | rage -r $hostPubkey > /tmp/agenix-rekey/"$(echo -ne "$originalSecretHash$hostPubkey" | sha256sum | cut -d " " -f 1)"
{ config, ... }:
let
originalSecretHash = builtins.hashFile "sha256" config.age.secrets.foo.rekeyFile;
hostPubkey = config.age.rekey.hostPubkey;
hash = builtins.hashString "sha256" "${originalSecretHash}${hostPubkey}";
cachePath = /tmp/agenix-rekey/${hash}; # Will have to be evaluated on the machine `agenix rekey` was run at but you might be able to wrap this in a builtins.readFile + pkgs.writeText to make it a derivation you can realise on the current host and then `nix3-copy` over
in {
activationScripts.whatever.text = ''
mkdir /run/agenix
rage -d -i /etc/ssh_host_key_location ${cachePath} > /run/agenix/foo
'';
}
EDIT: builtins.readFile + pkgs.writeText don't seem to fix the problem of changed hashes on different systems but my solution is cleaner imo
EDIT: you might achieve a similar effect by changing age.rekey.cacheDir
to the path type and removing UID stuff. I don't think it would require extra-sandbox-paths
or anything
https://github.com/oddlama/agenix-rekey/blob/main/modules/agenix-rekey.nix#L321-L323
>${lib.escapeShellArg file}.pub
I'm a little worried Nix would just make
file
a store path but I haven't looked at that part of the library's source code yet so maybe you have something to kill the string context or make rekeyFile a string when generating.
I deliberately remove the context here.
./secrets/id_ed25519
,./secrets/host1_id_ed25519.pub
Is there a reason the file paths don't end in
.age
?
No they should be .age
files. I just typed that from memory and forgot to add it :D
Btw is the rekeyed secret derivation hash really different depending on the target hosts's system?
Yes, because the derivation is a shell script and that depends on bash which depends on the system. I guess content-addressed derivations might solve this in the future.
https://github.com/oddlama/agenix-rekey/blob/main/nix/output-derivation.nix#L68 Why don't you just make a tempdir in the
agenix rekey
command wherever and let Nix copy the path (yes Nix path type)?
Because it is important that the output path of the secret derivation can be known without actually rekeying the secrets. How would you reference the current up-to-date rekeyed secret for a host in its configuration if you manually copied a path to the store? The rekeyed secrets must end up in the store after rekeying, so that you can deploy to other hosts without them having to care about agenix-rekey.
Currently the whole thing works like this:
forceRekeyOnSystem
to force other hosts to look into a derivation of the correct system type for the system you are actually rekeying with.agenix rekey
does the actual rekeying first and stores the results in the temporary path. That path has the .$UID
suffix so that you as an unprivileged user cannot interfere with secrets rekeyed by another user, which in theory could be used to hijack other users rekeyed secrets. Afterwards, it then proceeds to build the exact same derivation your system would depend on (but with access to the temporary path). Now the rekeyed secrets can be copied into the nix store by the bash script and will finally be available under the predictable store path of the derivation.I'm not 100% sure I followed your example, so please correct me if I am misunderstanding you.
https://github.com/oddlama/agenix-rekey/blob/main/nix/output-derivation.nix#L68 Why don't you just make a tempdir in the
agenix rekey
command wherever and let Nix copy the path (yes Nix path type)?Because it is important that the output path of the secret derivation can be known without actually rekeying the secrets. How would you reference the current up-to-date rekeyed secret for a host in its configuration if you manually copied a path to the store? The rekeyed secrets must end up in the store after rekeying, so that you can deploy to other hosts without them having to care about agenix-rekey.
- The derivation containing the rekeyed secrets has a trivial builder that copies rekeyed secrets from a pre-determined path. If these don't exist, the derivation deliberately fails and user is thus prompted to rekey.
Just thought of this again. Why don't you do something like this in agenix-rekey:
originalSecret=./secrets/foo
masterIdentity=~/agenix-identity
for host in a b c ; do
hostPubkey=./hosts/$host/hostkey.pub
rekeyedSecret=./hosts/$host/rekeyed/foo.age
rage -d -i $masterIdentity | rage -e -r $hostPubkey > $rekeyedSecret
done
# if flake is not referenced using path://
git add ./hosts/*/rekeyed
git commit -m "chore: rekey secrets"
{ config, ... }:
let
host = "a";
hostRekeyedDir = ./hosts/${host}/rekeyed;
in {
age.secrets.foo.file = let
path = /${hostRekeyedDir}/${config.age.secrets.foo.id}.age;
in if builtins.pathExists path then path else builtins.abort "Please rekey for host ${host}!"
}
As the flake goes to any eval hosts, ./hosts/*/rekeyed
should always work.
This way you don't need impurity and can build and/or evaluate on any host, even from a Git clone.
As the flake goes to any eval hosts,
./hosts/*/rekeyed
should always work.This way you don't need impurity and can build and/or evaluate on any host, even from a Git clone.
Ah, so what you are actually proposing is to store the rekeyed secrets in the repository. (Nitpick: It's still technically impure, because rekeying is the impurity and that's still the case - it just won't be triggered by building). What this changes is that it won't require you to build a derivation for the secrets, so this will eliminate all the problems with transferring the derivation, which is cool.
I do like the idea, it's obviously an advantage for CI builds and any multi-system flake. I guess I can add this when I have some time next month. :)
Since this will change the user's assumptions about how agenix works, I think we need to make this optional somehow. Currently I'm thinking of adding a mandatory configuration value where you have to specify storageMode = "repository";
or storageMode = "derivation";
. If the mode isn't set, it'll default to the old "derivation" mode and show a warning that you should decide for one or the other. I'm open to ideas.
I've added a local storage mode now, which should provide a solution to that. One disadvantage I noticed with the local storage approach is that you must be a little more careful with public repositories. If one of your host keys is ever leaked, an attacker can access all rekeyed secrets in the repo and decrypt them. Not very likely, but something to keep in mind.
I've added an upgrade notice and readme section for the new mode. This issue went a bit offtopic, do you think the original question is resolved so this can be closed?
If one of your host keys is ever leaked, an attacker can access all rekeyed secrets in the repo and decrypt them.
I didn't even think of that. Good catch!
I've added a local storage mode now, which should provide a solution to that. One disadvantage I noticed with the local storage approach is that you must be a little more careful with public repositories. If one of your host keys is ever leaked, an attacker can access all rekeyed secrets in the repo and decrypt them. Not very likely, but something to keep in mind.
I mean secrets (excluding MAC addresses and other PII) can almost always be changed. Btw, does the tool encrypt only the required secrets for each machine? The impact could be minimal compared to someone getting root on a box.
Have you thought of different ways of distributing keys? Wihout Nix derivations?
This issue went a bit offtopic, do you think the original question is resolved so this can be closed?
Uhh actually I can't think of any way to still do this. I was taking inspiration from nixus
I want to generate a private SSH key and distribute it to only one host. Then I want to generate a public key to use on only one NixOS host but
users.users.<name>.openssh.authorizedKeys.keyFiles
only takes in build-time paths. Is there a way to do this with agenix-rekey's generation system?