Sops-nix
5 min read
Atomic, declarative, and reproducible secret provisioning for NixOS based on sops.
Secrets are decrypted from sops files during activation time. The secrets are stored as one secret per file and access-controlled by full declarative configuration of their users, permissions, and groups. GPG keys or age keys can be used for decryption, and compatibility shims are supported to enable the use of SSH RSA or SSH Ed25519 keys.
Usage
Add sops-nix to your flake inputs and use it in a system configuration:
nix
{
inputs.sops-nix.url = "github:Mic92/sops-nix";
inputs.sops-nix.inputs.nixpkgs.follows = "nixpkgs";
outputs = { self, nixpkgs, sops-nix }: {
# change `yourhostname` to your actual hostname
nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem {
# customize to your system
system = "x86_64-linux";
modules = [
./configuration.nix
sops-nix.nixosModules.sops
];
};
};
}
Secret Management
You manage the secrets like you would in sops by declaring a .sops.yaml with creation rules and encrypted secret files.
To derive an age key from SSH host key:
shell
nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
For easier key management, you can seperate the secret files in their respective directories, use ssh host keys for decryption and reference them like:
yml
# .sops.yaml
keys:
- &root_key age1...
- hosts:
- &somehost age1...
creation_rules:
- path_regex: '\.secrets\/somehost\/.*(secrets?(\.yml)?)$'
encrypted_regex: '.*'
key_groups:
- age:
- *root_key
- *somehost
- path_regex: 'secrets?(\.yml)?'
encrypted_regex: '.*'
key_groups:
- age:
- *root_key
shell
# Encrypt a binary file
sops -e <file> > <encrypted.file>
# Edit a secret file
sops secrets.yml
# Update Keys
sops updatekeys <file>
Configuration
You first configure sops-nix:
nix
{
# This will add secrets.yml to the nix store
# You can avoid this by adding a string to the full path instead, i.e.
# sops.defaultSopsFile = "/root/.sops/secrets/example.yaml";
sops.defaultSopsFile = ./secrets/example.yaml;
# This will automatically import SSH keys as age keys
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
# This is using an age key that is expected to already be in the filesystem
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
# This will generate a new key if the key specified above does not exist
sops.age.generateKey = true;
}
Using Secrets
To use secrets use the following configuration options:
nix
# This is the actual specification of the secrets.
sops.secrets.example-key = {};
# The path here corrosponds with the fields path in the yml.
# ex. myservice/my_subdir/my_secret comes from myservice.my_subdir.my_secret and will be mounted at /run/secrets/myservice/my_subdir/my_secret
sops.secrets."myservice/my_subdir/my_secret" = {};
# More options
sops.secrets.example-secret {
# File Permissions (Mode)
mode = "0440";
# Owner + Group
owner = config.users.users.nobody.name;
group = config.users.users.nobody.group;
# Alternatively set owner/group by numeric id (useful for containers / DynamicUser)
uid = 65534;
gid = 65534;
# Restart systemd units when secret changes
restartUnits = [ "home-assistant.service" ];
# Reload (instead of restart) systemd units when secret changes
reloadUnits = [ "home-assistant.service" ];
# Symlink to other directories
path = "/var/lib/hass/secrets.yaml";
# Overwrite secret source per secret
sopsFile = ./other-secrets.json;
# Format (can be "json", "yml" or "binary" for placing binary secrets)
format = "json";
# Select a different key from secret source
key = "";
}
# Setting a user password
# This needs to happen before user setup. Generate a password with "echo 'pass' | mkpasswd -s"
sops.secrets.my-password.neededForUsers = true;
users.users.mic92 = {
isNormalUser = true;
hashedPasswordFile = config.sops.secrets.my-password.path;
};
Templates
Plain secrets are mounted one-file-per-secret, but a lot of software wants a whole config file (or env file) that mixes secret and non-secret values. Templates let you render such a file at activation time: you write a normal Nix string and drop placeholders where secrets should go. The placeholders (config.sops.placeholder.<name>) are opaque markers during evaluation, so the decrypted secret never ends up in the world-readable nix store — sops-nix substitutes the real value into the rendered file at activation.
Rendered templates land in /run/secrets-rendered/<name> and expose a .path you reference elsewhere.
nix
{
sops.secrets.your-secret = { };
sops.templates."config.toml" = {
content = ''
[database]
password = "${config.sops.placeholder.your-secret}"
host = "localhost" # non-secret values live here too
'';
# Same ownership/permission options as secrets
owner = "serviceuser";
group = "servicegroup";
mode = "0400";
# Optional: symlink the rendered file somewhere fixed
path = "/etc/myservice/config.toml";
# Restart units when the rendered output changes
restartUnits = [ "myservice.service" ];
};
systemd.services.myservice.serviceConfig.ExecStart =
"${pkgs.myservice}/bin/myservice --config ${config.sops.templates."config.toml".path}";
}
A very common use is rendering an env file for a systemd service that wants secrets as environment variables:
nix
{
sops.templates."myservice.env".content = ''
API_KEY=${config.sops.placeholder."service/api/key"}
API_SECRET=${config.sops.placeholder."service/api/secret"}
'';
systemd.services.myservice.serviceConfig.EnvironmentFile =
config.sops.templates."myservice.env".path;
}
If you only need to pass an already-dotenv-formatted secret straight into a unit, you can skip templates and point EnvironmentFile at a format = "dotenv" secret's .path directly. Templates shine when you need to combine multiple secrets and literals into one file.
Hardware-backed Keys (Age Plugins)
sops-nix can decrypt using age plugins, so the private key can live on a YubiKey or be sealed to a TPM instead of sitting on disk:
nix
{
sops.age.plugins = [
pkgs.age-plugin-yubikey
pkgs.age-plugin-tpm
];
}
Validation
By default sops-nix checks at evaluation time that the configured sops files are actually encrypted, so you can't accidentally commit/deploy a plaintext secret file. You can also set a default format globally:
nix
{
sops.validateSopsFiles = true; # default; verifies files are encrypted
sops.defaultSopsFormat = "yaml"; # default format for all secrets
}
Home-Manager
A Home-Manager module exists for per-user secrets. Secrets are decrypted by a sops-nix.service systemd user unit into $XDG_RUNTIME_DIR/secrets.d with stable symlinks under ~/.config/sops-nix/secrets. Use %r to refer to the runtime dir in paths, and order user services after sops-nix.service:
nix
{
imports = [ inputs.sops-nix.homeManagerModules.sops ];
sops = {
age.keyFile = "/home/user/.age-key.txt";
defaultSopsFile = ./secrets.yaml;
secrets.test.path = "%r/test.txt";
};
systemd.user.services.mbsync.Unit.After = [ "sops-nix.service" ];
}
Key Conversion Helpers
Besides ssh-to-age, sops-nix ships tooling for GPG-based setups:
shell
# Convert an RSA SSH key to a PGP key
nix-shell -p ssh-to-pgp --run "ssh-to-pgp -private-key -i ~/.ssh/id_rsa"
# Generate a server GPG key for sops
nix run github:Mic92/sops-nix#sops-init-gpg-key -- \
--hostname server01 --gpghome /tmp/newkey --keytype Curve25519