5 minutes
Getting NixOS to keep a secret
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.