← back to blog

NixOS and Secrets

· 8 min read

To start this off I will say that I’ve been using NixOS for 3 years now and I have used the main 6 (or 7 if we include ragenix) secrets management tools on NixOS. This consists of:

Before we go any further I want to say DO NOT use the last three if you intend to do share your machines or make your configuration public. This is because the nix store is world readable and people who have access to the machine will be able to read the secrets. I find this particularly pertinent at the time of writing with the following vulnerabilities: CVE-2026-31431 (copyfail) and CVE-2026-43284 and CVE-2026-43500 (dirtyfrag). For this reason I will not cover any of these options. But that is not to say I am guilt free in all of this. I have leaked my secrets on at least two occasions 1 2, and I’m sure you can find more if you look further.

sops-nix

So I have a bit of a love hate relationship with sops-nix. It was my first tool for secrets management but it was really hard to get into and working back when I originally started using it 3 years ago. Especially for a none technical user like I was at the time I really struggled to get it working. Which also eventually lead to me removing it since I didn’t understand it.

However, now I’m looking back it was easier than I realized, the docs have gotten miles better, and now sops natively supports using ssh keys to encrypt and decrypt secrets which is a huge improvement. Sadly sops-nix is lagging behind in support for this, see both sops-nix#779 and sops-nix#922.

The way you use sops-nix is by creating a yaml file with the rules for encrypting and decrypting secrets. An example of this might look like this:

.sops.yaml
keys:
- &isabel ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMQDiHbMSinj8twL9cTgPOfI6OMexrTZyHX27T8gnMj2
creation_rules:
- path_regex: secrets/*.yaml
key_groups:
- age:
- *isabel

Then you can use the sops command line tool to encrypt and decrypt the secrets. This command may look like sops secrets/shush.yaml. This will then open your chosen editor to allow you to configure a yaml file.

secrets/shush.yaml
hello: sops

Upon exiting the editor the data will then be encrypted and may look something like:

secrets/shush.yaml
hello: ENC[AES256_GCM,data:5ar0KQ==,iv:WpVEI/BetAloDP/9+4y28udJ04Loh4EBXFm5E8Sln7s=,tag:15IWu728tKQUYJHx9roVrQ==,type:str]
sops:
age:
- recipient: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMQDiHbMSinj8twL9cTgPOfI6OMexrTZyHX27T8gnMj2
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IDk1NDQzZyBuYnhk
djVoS2F4dHZMUE9LUE5xb3htamVDTnpoTVBPN05BOEZheFJ6a3hJCk1tZ0JoMlhJ
WWdrVEViUC91VXk3emVETUdSV2tDcHd5dTlKYlJjWGhxcm8KLS0tIFlaczBRRXVQ
emtZZlUvdEFhU29YSnc3dHNOWHdlamtORCtwN0wxdGQ2ekUKNYpnUt83rFILe/A2
RiXGYQoDTj3NF6t5szFWeCWXftWZFmsLBhQ59PDpfnrk+cHWXILhxIifrJjlDoHh
9+i8Yw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-08T15:21:51Z"
mac: ENC[AES256_GCM,data:o7aa6vz7qAkS93XPK9adlT5b5382n5c1egTGGft847mYkCM6A2TAOQhMdcrHsN90aY7f64rglt0LaKFrUBOAh8hN04cSvNLykJ7iYYFq+rnADt3HQbjyVcYcZKTeMJ+797Uus26CW24reFENTtqum6VeL1FU78bVEh6/eS03V0E=,iv:Z2w4RbPC4c16VvxAPi4kydR+cNoEkKr4KsXoKHjn+OY=,tag:i+2eSHAscqBvHdZI9T250A==,type:str]
unencrypted_suffix: _unencrypted
version: 3.12.2

This is totally normal, and you can use the same command as you used before to keep editing them.

Once the file is encrypted you will still need to introduce your config to it. sops-nix ships a module that does the heavy lifting, so a typical config may look like:

{
sops = {
defaultSopsFile = ./secrets/shush.yaml;
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
secrets."hello" = {
owner = "isabel";
group = "users";
mode = "0400";
};
};
}

At activation time sops-nix decrypts the file using the host’s SSH key and drops the plaintext at /run/secrets/<name>, which is a tmpfs so the secret never touches disk. Anything that needs the value just reads that path.

Another feature I lean on heavily is templates. This is particularly useful if your config is shared between users or referenced by others. But there is still use outside of that, for example if a service wants a config file that mixes plain text with one or two secret values, you don’t have to encrypt the whole file:

{ config, ... }:
{
sops.templates."mailserver.env".content = ''
SMTP_USER=isabel
SMTP_PASSWORD=${config.sops.placeholder."mailserver/smtp_password"}
'';
}

Agenix

Agenix takes a different approach from sops-nix making it feel a lot more like nix since you configure all the secrets and who can access them through the secrets.nix file. It is important to note that you can also configure what keys have access to what secret. This file might look a little like so:

secrets.nix
let
isabel = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0idNvgGiucWgup/mP78zyC23uFjYq0evcWdjGQUaBH";
host1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPJDyIr/FSz1cJdcoW69R+NrWzwGK/+3gJpqD1t8L2zE";
host2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKzxQgondgEYcLpcPdJLrTdNgZ2gznOHCAxMdaceTUT1";
in
{
"secret1.age".publicKeys = [ isabel host1 ];
"secret2.age".publicKeys = [ isabel host2 ];
}

From this point you will have to add the secret1.age and secret2.age files. This should be done with the agenix cli, the command may look like agenix -e secret1.age, this same command can be used later in order to edit the files.

Wiring agenix into a host looks fairly similar to sops-nix, but the surface area is smaller because each secret is its own file:

{
age.secrets.secret1 = {
file = ./secrets/secret1.age;
owner = "isabel";
group = "users";
mode = "0400";
};
}

At boot the host’s SSH key is used to decrypt each .age file into /run/agenix/<name>, again on a tmpfs. The bit that catches most people out is rekeying. Every time that you add a new host or rotate a key, every secret in secrets.nix whose publicKeys list has changed will need to be re-encrypted. The agenix --rekey command will do this for you, but it needs the current private key for one of the recipients to read the existing ciphertext first. In practice this means rekeying happens on the machine you trust most, not on the new host you’re trying to bring up.

Using the filesystem

The cost of this method being that your config no longer fully describes your machines, which is why I’ve never attempted to use this method. If you reinstall, you have to remember to put every one of those files back in the right place with the right ownership. It also makes it a total disaster when doing recovery, which matters more than you’d think when you’re rebuilding a server at 2am.

The thing to avoid is builtins.readFile "/var/lib/myservice/token" or similar. That reads the file at evaluation time and embeds the contents into the nix store, which is world-readable and is exactly the failure mode the intro warned about. Always pass the path to the service via options like services.*.environmentFiles.

For a single server or laptop this maybe fine. For anything you’d describe as a fleet, or anything you want to be able to rebuild from scratch from just your config, use sops-nix or agenix instead.

The Battle between the big two

The main reason to use sops-nix is that you’re packing as much data into one file as possible, which has its own respective pros and cons. For me that mostly meant that I could put a lot more of my mail server secrets into one file rather than having them split up between 5 or so files like with agenix.

Agenix wins in terms of simplicity. There’s no yaml schema to learn, no .sops.yaml to keep in sync, and the secrets.nix file is just nix, so the same let ... in bindings you already use for hosts and users work for keys. The mental model is “one secret, one file, one list of recipients”, and that maps cleanly onto how I think about access control.

The honest answer is that both tools solve the problem and the difference at this point is mostly ergonomics. If you’re starting from scratch and you have more than a couple of services that each want a bundle of related secrets, sops-nix will scale better. If you’re starting from scratch and you mostly have a handful of standalone tokens, agenix will get you there with less ceremony.

It is also important to note at this current moment of time agenix is NOT post quantum safe. However, the same issue applies to sops-nix but not because of a limitation of sops like agenix’s limitation in the age cli. But rather due to sops-nix not supporting Post-Quantum age keys sops-nix#885.

Since writing I have been corrected in saying this, as age and sops do support post-quantum secure encryption keys. Since age#578 was closed and v1.3.0 was released. When generating your age key change the command to include -pq so the command might now look like age-keygen -pq -o key.txt.

Conclusion

After three years of cycling through every option on the list, the picture I’ve landed on is roughly this:

If you’re picking your first secrets tool, pick agenix, get comfortable with the flow, and only graduate to sops-nix once you actually feel the pain of one secret per file.

share this post