Nix secrets for dummies

Published 2024-07-12 on Farid Zakaria's Blog

Despite having used Nix for several years already, I feel secure in admitting that new concepts in Nix continue to be confusing at first glance.

I had avoided doing any secret management for Nix for a long time, largely because I ran Nix atop another Linux distribution, but now that I’m on NixOS my time hath come.

I wanted to write out my learnings here so that others may benefit from the ELI5 style of learning that we are missing in Nix.

Doing any basic Google-fu lands on two solutions for Nix for secret management: agenix or sops-nix.

Let me save you time, and we will focus solely on agenix. agenix is a Nix module + helpful tooling built around age.

We will work around the example of a secret file: password

❯ echo -n "swordfish" > password
❯ cat password
swordfish

age’s popularity is all centered around it’s minimalism and ease-of-use.

You encrypt files in age with “recipients” (public-keys) and decrypt them with “identities” (private-keys).

❗You can encrypt a file for multiple recipients but any single one can decrypt the file. This will prove to be useful for agenix.

❯ age --armor \
    -r age1yubikey1qg8nf40dfw4gprmywplggtg2wuvv55fcmujzrm65z8s3j6rhwje2vm3hhs7 \
    password > password.age

❯ cat password.age 
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHBpdi1wMjU2IGVUczBkQSBBeEIwTlVC
TE1ZU3dhS1I4UlphT1dhaXNUY2Z2L1pLaWUxS0dwZFB6V0x4VApLWCs5R2hjY3JT
V2RxTk13RFE0bDNRZXZ1NVJWUzhrQm05dlNsSTN2eSs0Ci0tLSB4eUpVaC9FUm56
OEJQaVp0aEFRS2VZSlJQYWl0dGU5a2dpYVhOb2hsN05NCprozK/msLjTafWAkrSe
+wYeAyI82vEwa0d5MO/CZzxwixKlRKbzZ2flKg==
-----END AGE ENCRYPTED FILE-----

recipients can be an age public key generated by age-keygen (“age1…”), an SSH public key (“ssh-ed25519 AAAA…”, “ssh-rsa AAAA…”) or even a private key stored on a Yubikey using age-plugin-yubikey.

How does this all fit in with NixOS ?

The goal is to get an encrypted secret from one machine to another. agenix takes advantage of the fact that most systems built with NixOS have a default host key generated via services.openssh.hostKeys.

❯ ls /etc/ssh/*_key
/etc/ssh/ssh_host_ed25519_key  /etc/ssh/ssh_host_rsa_key
❯ ls /etc/ssh/*_key*pub
/etc/ssh/ssh_host_ed25519_key.pub  /etc/ssh/ssh_host_rsa_key.pub

You can also use ssh-keyscan localhost to get the public keys for the current host or any host (just change localhost to the desired one).

🤔 We can encrypt our secret with our own personal key and the keys of any machine we’d like to deploy to. Any single key can decrypt the secret.

agenix sets up the mapping of which keys to use to encrypt each secret via the file secrets.nix

I found the installation instructions for agenix great so they are not covered here.

The systems keys should be what’s inside /etc/ssh/ssh_host_ed25519_key.pub

The users key could be a custom supplied private key (Yubikey) or by default will search SSH key in ~/.ssh/id_ed25519

let
  # put the machine you want to deploy to here
  systems = {
    nyx = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXmzOre8wnaZm4zXuXqzFRS+5GFlMyfhth9ie9AvW8t root@nyx";
  };
  # put which users should also be able to decrypt the secret
  users = {
    fmzakari-yubikey = "age1yubikey1qg8nf40dfw4gprmywplggtg2wuvv55fcmujzrm65z8s3j6rhwje2vm3hhs7";
  };
  allUsers = builtins.attrValues users;
  allSystems = builtins.attrValues systems;
in {
  "password.age".publicKeys = allUsers ++ [systems.nyx];
}

You run it with agenix -e <FILE> where FILE is one of the keys in the attrset in secrets.nix.

🤔 This mapping is not specific to Nix and could even be a TOML file with a very minimal secrets.nix

["password.age"]
publicKeys = [
  "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXmzOre8wnaZm4zXuXqzFRS+5GFlMyfhth9ie9AvW8t"
  "age1yubikey1qg8nf40dfw4gprmywplggtg2wuvv55fcmujzrm65z8s3j6rhwje2vm3hhs7"
]
builtins.fromTOML (builtins.readFile ./secrets.toml)

❗ You don’t even have to use the agenix CLI tool if you want to just list the recipients yourself and use age to encrypt secrets.

You should now have password.age. Feel free to commit this to your repository.

How do we make use of this secret? That’s where the agenix NixOS module comes into play.

Simply, declare which secrets you want to try to decrypt. agenix will use the target machine’s SSH private key to decrypt the secret.

Remember we encrypted the secret with host public keys of the machines that could decrypt it!

  age.secrets = {
    "password" = {
      # change to whatever path it is in your NixOS configuration
      file = ./secrets/password.age;
    };
  };

After you deploy your NixOS configuration, you can find the secret at /run/agenix decrypted at switch time via an activation script.

❯ sudo ls /run/agenix/
password

You can read from the filesystem or reference this path in Nix itself.

${config.age.secrets."password".path}

🎆 You now have unlocked a new level in NixOS: Secret Management.

If you think this post can be improved please let me know