In the land of NixOS all roads lead to the Nix Store.

Everything you put in your .nix files, any input files/directories said .nix files reference, and all the build output of the derivations said .nix files define will end up in your nix store. And this can be a problem.

You don’t want things like api tokens and other credentials ending up in a world readable location like /nix/store, or worse still being pushed to a remote store or binary cache. Neither do I, which is why until recently I avoided the problem altogether by keeping my access tokens and so on inside a Pass store.

That was, until last week, when I decided to finally learn how to do this safely within Nix land and came across sops-nix.

A wrapper for SOPS: Secrets OPerationS from Mozilla, sops-nix integrates SOPS with NixOS in such a way that you can safely include secrets and files within your system config and not worry about them leaking.

Getting started

Your secrets will live in YAML files within a directory you specify. I stick them in a secrets folder. There can be multiple secrets files nested within this directory. You’re not restricted to YAML, you can use JSON or binary files too.

What is most important is how you tell SOPS to encrypt these files. Below is an example from my personal setup (redacted where appropriate 😉):

keys:
    - &user_brian_yubi-alpha AE06...
    - &user_brian_yubi-beta 4DA3...
    - &host_saturn age1gkq...
    - &host_mimas age1uk0...
    - &host_enceladus age16q0...
    - &host_vm age1us0...
creation_rules:
    - path_regex: secrets/[^/]+\.yaml$
      key_groups:
          - pgp:
                - *user_brian_yubi-alpha
                - *user_brian_yubi-beta
            age:
                - *host_saturn
                - *host_enceladus
                - *host_mimas
                - *host_vm

This is telling SOPS that I want to encrypt any YAML files within my secrets directory with a combination of the PGP and age keys I’ve specified, with PGP being used by my user and ssh host keys converted to age format for my hosts.

Now when I invoke SOPS it will know which key groups to use based on the file location and type within my secrets directory.

Creating secrets

Creating a new file is pretty straightforward: sops secrets/foo.yaml. This will open your favourite editor and present you with some sample data:

hello: Welcome to SOPS! Edit this file as you please!
example_key: example_value
# Example comment
example_array:
    - example_value1
    - example_value2
example_number: 1234.56789
example_booleans:
    - true
    - false

Edit this file as you see fit and save it. Upon exiting SOPS will then encrypt the contents using each of the keys in the key group as defined by the .sops.yaml file. The file secrets/foo.yaml will end up looking something like this:

hello: ENC[AES256_GCM,data:9zOtsm8=,iv:44s+wBPKrNxUFuadfkD4fMrdYU8t+f3EJ+1b/20sH00=,tag:7K0Sf5o7lf266J1XTJY06A==,type:str]
sops:
    kms: []
    gcp_kms: []
    azure_kv: []
    hc_vault: []
    age:
        - recipient: age1gkq...
          enc: |
              -----BEGIN AGE ENCRYPTED FILE-----
              YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtNDRsVE1ybzUvME9FaW9t
              cGxNdnVWUkt0RFhTN0l6VHJwMG4rVVJQeDB3CnNCM3RaZVNIUjdWVDY4YWZGL25D
              KzIzbHJmQ1l6bkJwS2NGTnJIdkYvNFUKLS0tIFh6aVlXaDNXMGZGSksyQks5WGJL
              M2dlem5IQUQ5SklxYlFyektQYjF0eGMK/5P7d7EO+YO3FqejzloWjgbMWRExDmVj
              B/xhgfhEG2YqLJPcGtStkN+SB0XmnaMpuDLU8GPXKv93kYXVD9ySYg==
              -----END AGE ENCRYPTED FILE-----              
        - recipient: age16q0...
          enc: |
              -----BEGIN AGE ENCRYPTED FILE-----
              YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhdjdLSzF0dUJGZmM2NU5L
              NVVteHpFclFob1dZVzZQenppRURnYVo3RDBVClR1a1QzMGZZdElqbHlCTTQ5Mzdk
              RUwzSjFOY1VFQkxDT05yODlITWsvNk0KLS0tIGwxekJqQkhSd0F6aUkvaE9JUFZC
              ckFDaS9lYWQwaktkbUZud3VSRW5zNGsKnlEAoAOkKsTLzyvzMGGTSouaPhadEYCz
              TJgJeQJTAkvLfEol8727/NCSIU7E/PSyEwkml2+tFF2KZdUjkIJ2Jw==
              -----END AGE ENCRYPTED FILE-----              
        - recipient: age1uk0...
          enc: |
              -----BEGIN AGE ENCRYPTED FILE-----
              YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzajF2UU5vOUVsdHgzdHpS
              OEtiZWg5YzB2aGlXZUdBZ2MwY2U2Qnp6d0M0Cm9sNXFzNXowRGFoQjc3TUtiOUk0
              SkRIbXN6NzRuVlpiVklmS3JZdUtMQVEKLS0tIFFmOWpuNG40ZTRVQXdwWm5EdDla
              bG5Hait6UXU1NWxPR0JpbHdmMWdrSzAKPjXC3/F7YcTqdALcHw30LZw3MsWII0HP
              e70SoVehqg3PrcUkj8gbyd4sxFKRi1IrULHmhN4fLV0UapN3XG/+Ew==
              -----END AGE ENCRYPTED FILE-----              
        - recipient: age1us0...
          enc: |
              -----BEGIN AGE ENCRYPTED FILE-----
              YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpcFhuMG80RDFtdmExSTRQ
              Y0FpdDZxZTFKdFpMYnJxaEVCUlFxeUN2M0FBCkt2dHZMejFqa2p3RHdvQ3RXTm9j
              SEdyMzNidHdRK1RvWVQ2b3d1Wk0rQTgKLS0tIG92SVhCbE1ZQmNScHFOMEZuVGdF
              R00yMzZTRENlWFRyRkloRWUvZDN6WUEKbcQj09GLbkV7D3ka3wZvZNvhS8hs9vcg
              byJODavyRvN1YsT6Eznlb2WZOed7nlKm1vllf4HIORzTQAoTN6Y/wg==
              -----END AGE ENCRYPTED FILE-----              
    lastmodified: "2022-11-25T15:59:42Z"
    mac: ENC[AES256_GCM,data:nk72oBH8oA2LV6PhbXkmtUs30WlKYOBcRyxH3y3i4tD21J1na7n/a012ctt+kknXMgXbIWmTYxXOE1IW/BlcncXWXCCAQjABcnT6atzDPjd21cBTEmXm2D/nJJIokZGaH8q8TKDWUD95dZ4pESk25l4aQnXdZBXpZowf+q1N8gs=,iv:vBbLZ1ZHgLGs6mJz34W/f0Pw8PofU0fVJuUV7Wf9iMg=,tag:sUjCBG8olBSFaa86wVLoQQ==,type:str]
    pgp:
        - created_at: "2022-11-25T15:56:31Z"
          enc: |
              -----BEGIN PGP MESSAGE-----

              hF4DZvJpBuYi3oMSAQdAxyjifCbNYNwBK+HfAn7mBT8+JmldOIw3iISC1JLjgXAw
              dV9xQXiIMALnQjqoDY3BQ4ClOChv6+Qlg6p2z4ks8sMvZt+cdn4Xa43awiMXRPEc
              0l4Bhc58148nSHvOkYsj8n2PuCLioXN2vKNvJgRm2C/gibdxUE7iVoardTOlDVrP
              uO4kyBGQhXG031B6ZxgfNTT1fWzwwxuVHhZNDa8AIf4/H9xIdYmxMqzlr2ZYdBst
              =eD5H
              -----END PGP MESSAGE-----              
          fp: 38DA...
        - created_at: "2022-11-25T15:56:31Z"
          enc: |
              -----BEGIN PGP MESSAGE-----

              hF4DZvJpBuYi3oMSAQdApZSHfbnJTq7UyUx/EgT+DfqCBhhoc8CoF50cilunOF8w
              Lz6m98uXwsrTgEKOxZvhtGO4p1xYIvG9LPQKz9RUC/jsQF4gitmnS3vfs2Ytaqsb
              0l4BwTj3U36Sik2my3TcInEsyllR0iWvtuMVTO29jr0CZK+2LyiGieVrKW4XsiXH
              18p3lNZVBeFKIrBlfhP0/vbS//VSSaqQtBWDy+tmekSYCAqf8Qv3dcGGNo7yitYC
              =0MH1
              -----END PGP MESSAGE-----              
          fp: AE065...
        - created_at: "2022-11-25T15:56:31Z"
          enc: |
              -----BEGIN PGP MESSAGE-----

              hF4DZvJpBuYi3oMSAQdAsEBeGO8VulNICOEHMpyEG6sju5EkSyz8emCUxy1YWzgw
              9R/wbtbZV2QNE5vnUgIYTn59p6AUrIqRCIF2dDDdQCuguMYRlIij+RSKyc66vWS/
              0l4Bi+fw58hr6CrXaG4Lly2FPvjuPwbwOCIb6K3+kNjiSCpgsLRuypPogze1vT9+
              XJYRIqeVqJNePjYk3HWvY6zuWAMIkuAHAT4VumYmnnCMTBBIrIZFwr0WT1cGVE7J
              =ws6L
              -----END PGP MESSAGE-----              
          fp: 4DA3...
    unencrypted_suffix: _unencrypted
    version: 3.7.3

Since it is encrypted at rest, this file can be safely checked into source control. And since it’s text based you can easily track how it has evolved over time. But most importantly for us, this file can be safely placed within the Nix store.

Integrating with NixOS

Now that we have our secrets encrypted, we need a way to use these secrets from within our system config. For that sops-nix provides a NixOS module which will take care of decrypting secrets on startup based on the host ssh key and making them available within a directory structure under /run/secrets.

With it we can define what secrets we want to make available within our system config and from which files to source them.

For example, the following defines a github_token secret and specifies which user and group can access it:

{
  # import the module
  imports = [ <sops-nix/modules/sops> ];

  # specify which sops file to use for the secrets
  sops.defaultSopsFile = ../../secrets/secrets.yaml;

  # configure which secrets to make available and how
  # note: github_token must be a key within the secrets.yaml file
  sops.secrets = {
    github_token = {
      owner = brian.name;
      group = brian.group;
    };
  };
}

I can then use this secret in a Home Manager module to load it into my shell environment like this:

{...}: {

    programs.zsh.initExtra = ''
      if [[ -o interactive ]]; then
          export GITHUB_TOKEN=$(cat /run/secrets/github_token)
      fi
    '';

}

Alternatively you can reference a secret as a variable within your nix config like this:

{
 config = {
    users.users.brian = {
      passwordFile = config.sops.secrets.password.path;
    };
  };
}

I think we can all agree this is pretty sweet 😎.

Summary

I’ve talked about the dangers of allowing secrets to leak into your NixOS system config, and provided a short introduction to sops-nix and how it can help to avoid this problem.

It’s worth pointing out though that there are a variety of options when it comes to managing secrets within NixOS, and I would suggest evaluating them for yourself.

But if you’re asking me which would I recommend, I’d say stick with sops-nix. And if you’re looking for a more exhaustive introduction to sops-nix I would suggest reading the excellent README that Mic92 already included in the repo.