Using SOPS for Encrypted Configuration (sops-nix)
Using SOPS for Encrypted Configuration (sops-nix)
Secrets that live in a wiki are missing on the next host. The boring default is: SOPS YAML in git (keys visible, values encrypted) and sops-nix decrypting to /run/secrets at boot.
Mental model
Mozilla SOPS encrypts values in YAML/JSON. Key names stay plaintext so diffs stay reviewable (database/password: still reads as that path). Recipients are Age keys (or SSH host keys converted with ssh-to-age).
sops-nix is a NixOS module:
- Reads
defaultSopsFile. - At activation/boot, decrypts with the host Age key (
age.keyFileor SSH host key). - Writes files under
/run/secrets/…withowner/modeyou set. - Units reference
config.sops.secrets.<name>.path.
secrets/desk.yaml (cipher in git)
│ sops-nix + host age key
▼
/run/secrets/database/password (0400, tmpfs)
│
▼
desk-api EnvironmentFile=
Worked examples
Case 1: Recipients and a secrets file
Save as .sops.yaml:
# .sops.yaml
creation_rules:
- path_regex: secrets/.*\.yaml$
key_groups:
- age:
- age1deskadminxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- age1desknode01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxmkdir -p secrets
sops secrets/desk.yamlExample decrypted editor view (what sops shows you):
database:
password: please-change-meOn disk in git, the value is ENC[AES256_GCM,data:…]. Commit both .sops.yaml and secrets/desk.yaml.
Case 2: NixOS module
Save as secrets-module.nix:
# secrets-module.nix
{ config, inputs, ... }:
{
imports = [ inputs.sops-nix.nixosModules.sops ];
sops = {
defaultSopsFile = ./secrets/desk.yaml;
defaultSopsFormat = "yaml";
age.keyFile = "/var/lib/sops-nix/key.txt";
secrets."database/password" = {
owner = "desk-api";
mode = "0400";
};
};
systemd.services.desk-api.serviceConfig.EnvironmentFile =
config.sops.secrets."database/password".path;
}If the YAML value is a raw password, EnvironmentFile is the wrong shape (that file is KEY=value lines). Store a dotenv in sops (DESK_DB=…) or use a template:
# sops-template.nix fragment
{
sops.templates."desk-api.env" = {
content = ''
DATABASE_PASSWORD=${config.sops.placeholder."database/password"}
'';
owner = "desk-api";
mode = "0400";
};
systemd.services.desk-api.serviceConfig.EnvironmentFile =
config.sops.templates."desk-api.env".path;
}placeholder is ciphertext-safe at eval: Nix never sees the live password. restartUnits = [ "desk-api.service" ]; on the secret so a rekey + switch restarts the unit.
nix eval --raw .#nixosConfigurations.desk-server.config.sops.secrets.\"database/password\".pathOutput:
/run/secrets/database/password
Case 3: Age key from SSH host key
# sops-ssh.nix
{
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
}No extra key to distribute if the host already has an Ed25519 host key and that key is in .sops.yaml (via ssh-to-age). Persist the host key under impermanence or every reboot is a new recipient.
ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pubPaste the age1… line into .sops.yaml, then sops updatekeys secrets/desk.yaml.
Case 4: Flake input
# flake.nix fragment
{
inputs.sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
}follows so sops-nix does not pull a second nixpkgs.
Case 5: Missing owner
If desk-api runs as DynamicUser and the secret is 0400 root, the unit fails with Permission denied. Set owner to the service user, or use sops.secrets.x.restartUnits = [ "desk-api.service" ]; plus a group both can share.
sudo ls -l /run/secrets/database/password
sudo systemctl status desk-apiThe trap
The trap is encrypting only to your laptop Age key. The server cannot decrypt at boot. Always include the host recipient (and the admin laptop, and CI if CI must eval decrypt).
The other trap is age.keyFile on a path that impermanence wipes. Persist /var/lib/sops-nix or use the SSH host key that you already persist.
The boring rule
- Encrypted YAML in git. Recipients: admins + each host.
- Units consume
.pathorsops.templates. Never decrypted strings in Nix. CIflake checkmust not need the private key. - Host Age key = SSH host key when you can. Persist that key.
- Set
ownerandmodeto match the unit. sops updatekeyswhen a machine or person is added or revoked.
Try this
age-keygen -o /tmp/k.txt, add the public key to.sops.yaml,sops secrets/demo.yamlwithhello: world,sops -d secrets/demo.yaml.ssh-to-age < ~/.ssh/id_ed25519.puband encrypt a file to that recipient.- Eval
.pathfrom a flake that imports sops-nix. Confirm it starts with/run/secrets. - Intentionally omit the host from
.sops.yaml, rebuild a VM, read the sops-nix unit log. Add the host andupdatekeys.