splitbrain.org

electronic brain surgery since 2001

NixOS Agenix (for secrets management)

not the agenix logo A couple of days ago, I ended my post on setting up Postfix on NixOS with the remaining question of secrets management.

Why?

The issue with the way how Nix works is that everything1) is copied to the “nixstore”2) and then symlinked around the system. However, apparently that store is world readable. So any process on the system can access it. Doesn't matter for most stuff, but is a bit shitty when it comes to secrets.

So one option is to keep your secrets out of the nixstore by not adding them directly to your config. Instead have it reference a file which you then protect by standard Linux ownership and permissions. That's the way I chose in the last blog post.

Or you can use some secrets management tool. There are only two candidates and agenix seems to be the more popular and simpler one.

So how does it work? agenix uses the age encryption tool under the hood. That is an asymmetric encryption tool similar to gpg but with less cruft and the additional advantage that it can use SSH keys instead of requiring separate key pairs.

The idea is to encrypt all your secretes with age, agenix hooks into the nix rebuild process, decrypts and copies the plaintext versions to it's own agenix store and sets restrictive permissions on them. Your nix configuration (in the nixstore) then references those files.

Key Setup

age (and thus agenix), does not work with the ssh-keyagent3). So for password protected keys you would need to enter your password every time when accessing a secret.

Luckily that doesn't matter too much because you will only need your personal key in emergencies.

Since age is asymmetric encryption, only the public key is needed to encrypt a file and the private key is needed when decrypting. In addition, age can encrypt for multiple target keys - but only one of the matching private keys is needed for decryption.

The agenix module (hooked into your configuration.nix) will use the system's host key to decrypt. During normal working with your config (eg. when creating new secrets) we can use the local root account's key. And only if we need to access keys from a backup, with the root and host keys lost, is having an additional personal key useful.

The host key will be created automatically when the SSH service is enabled. Get it from /etc/ssh/ssh_host_ed25519_key.pub.

The root key needs to be created:

# ssh-keygen -t ed25519 -C 'NAS root user'
Generating public/private ed25519 key pair.
Enter file in which to save the key (/root/.ssh/id_ed25519): 
Enter passphrase for "/root/.ssh/id_ed25519" (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /root/.ssh/id_ed25519
Your public key has been saved in /root/.ssh/id_ed25519.pub

Be sure to add no password. Get the key from /root/.ssh/id_ed25519.pub.

With those two and your personal key, create the agenix configuration file:

/etc/nixos/secrets/secrets.nix
let
  keys = [
    # /root/.ssh/id_ed25519.pub
    "ssh-ed25519 AAA...cxy1w"

    # /etc/ssh/ssh_host_ed25519_key.pub    
    "ssh-ed25519 AAA...KafTS"

    # personal key
    "ssh-rsa AAA...DzBA9Qw=="
  ];
in
{
  "example.age".publicKeys = keys;
}

You can of course add any additional arbitrary keys.

Note: the above syntax is someone simpler than what you might see elsewhere. Others use associative arrays or even more deeply nested structures to name and organize the keys. It really doesn't matter. At the end you need to assign an array of keys to the .publicKeys property of the different secret files.

We will replace the example.age setting at the bottom later.

Install agenix

agenix consists of two separate pieces: a library or module to hook into the NixOs rebuild mechanism and a command line tool.

To install the module add the fetchTarball command to your imports:

/etc/nixos/configuration.nix
  imports =
    [ 
      ./hardware-configuration.nix
      "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age.nix"
    ];

The CLI tool is installed similarly but in the systemPackages array:

/etc/nixos/configuration.nix
  environment.systemPackages = with pkgs; [
    vim 
    wget
    ...
    (callPackage "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/pkgs/agenix.nix" {})
  ];

I find it a bit weird that those things are not available in the standard distribution and have to be pulled via tarballs from Github4).

Do a nixos-rebuild switch and you should have the agenix command available.

Creating Secrets

Time to create a new secret.

All secrets need to be listed in the secrets.nix file first, so agenix know what keys to encrypt for5). Let's add a line for the postfix-sasl_passwd file from the last post:

/etc/nixos/secrets/secrets.nix
let
  keys = [
    ...
  ];
in
{
  "postfix-sasl_passwd.age".publicKeys = keys;
}

With that in place you can edit the file:

cd /etc/nixos/secrets/
agenix -e postfix-sasl_passwd.age

This will open $EDITOR and lets you edit the file. On saving it will be encrypted.

Using a Secret

Now the official docs will tell you to pull the secret into your configuration first and then use it. But that's kinda silly when we have to configure all the secrets in the secrets.nix file already.

So instead of pulling in secrets one by one, we can simply import them all6):

/etc/nixos/configuration.nix
  # import all agenix secrets
  age.secrets = let
    secrets = import ./secrets/secrets.nix;
  in
    builtins.mapAttrs (name: attrs: {
      file = ./secrets/${name};
      owner = attrs.owner or "root";
      group = attrs.group or "root";
      mode = attrs.mode or "0400";
    }) secrets;

With that in place we can reference all secrets by their filenames. For my postfix config that becomes:

/etc/nixos/configuration.nix
  services.postfix = {
    enable = true;
    ...
    extraConfig = ''
      smtp_sasl_auth_enable = yes
      smtp_sasl_password_maps = texthash:${config.age.secrets."postfix-sasl_passwd.age".path}
      local_header_rewrite_clients = static:all
      append_dot_mydomain = yes
    '';
    ...
  };

The .path will point to the decrypted file in the agenix store. By default all decrypted files are owned by root:root with permission 0400.

If you ever need to change that, you can configure it in your secrets.nix file:

/etc/nixos/secrets/secrets.nix
let
  keys = [
    ...
  ];
in
{
  "postfix-sasl_passwd.age".publicKeys = keys;
  "example.age"= {
    publicKeys = keys;
    owner = "joe";
    group = "users";
    mode = "0660";
  };
}

Overall this works fine, but I don't understand why NixOS doesn't have a builtin solution here.

The whole setup also only works when your services expect secrets in an extra file. If they, for example, use a single monolithic configuration file, you would need to put the whole config into the secret file…

Tags:
nix, nixos, agenix, linux
Similar posts:
1)
I believe that includes binaries, resources, config files, etc.
2)
some directory
3)
it boggles my mind that this has not been implemented
4)
Nix people will tell you it doesn't matter and you should be using flakes anyway
5)
there is no way to set a set of default keys
6)
I see the advantage of nix being a programming language rather than a config syntax

Comments