Skip to content

Manage secrets with sops-nix

sops-nix decrypts secrets that are committed to your repository in encrypted form and installs them, at runtime, into a ramfs under /run/secrets. System Manager supports it, so encrypted secrets can live in Git while the plaintext never touches disk.

How it works here

On NixOS, sops-nix normally installs secrets from an activation script. System Manager does not run NixOS activation scripts: it activates a configuration by writing systemd units and restarting sysinit-reactivation.target.

Instead, System Manager uses the systemd code path that sops-nix already provides (sops.useSystemdActivation), which installs secrets from a sops-install-secrets.service oneshot unit. This is enabled by default and is ordered so that secrets are installed both on boot and on every system-manager switch.

Basic usage

Add sops-nix as a flake input and import its NixOS module:

1
2
3
4
{
  inputs.sops-nix.url = "github:Mic92/sops-nix";
  inputs.sops-nix.inputs.nixpkgs.follows = "nixpkgs";
}
{ config, ... }:
{
  imports = [ inputs.sops-nix.nixosModules.sops ];

  sops = {
    age.keyFile = "/var/lib/sops-nix/key.txt";
    defaultSopsFile = ./secrets.yaml;
    secrets.my-service-env = { };
  };

  systemd.services.my-service = {
    wantedBy = [ "system-manager.target" ];
    serviceConfig.EnvironmentFile = config.sops.secrets.my-service-env.path;
  };
}

The secret is decrypted into /run/secrets/my-service-env, owned by root with mode 0400 by default.

Choosing a decryption key

Because System Manager does not manage services.openssh, sops-nix cannot auto-detect SSH host keys. Pick one of:

  • An age key file — set sops.age.keyFile to a path outside the Nix store, and provision the key out of band.
  • An SSH host key — set sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; explicitly to reuse a key the machine already has.
  • A generated age key — set sops.age.generateKey = true; together with sops.age.keyFile. System Manager generates the key from a sops-generate-age-key.service oneshot ordered before secret installation. The key is only generated if the file does not exist yet, so you have to add its public half to your .sops.yaml and re-encrypt before secrets can be decrypted on that machine.

Restarting services when a secret changes

sops.secrets.<name>.restartUnits and reloadUnits work as they do on NixOS: when the decrypted value changes, sops-install-secrets acts on the listed units through systemctl directly, rather than through the NixOS activation-restart lists that System Manager does not have.

sops.secrets.my-service-env.restartUnits = [ "my-service.service" ];

Two details are worth knowing, because they follow from how the unit is ordered:

  • The verbs used are try-restart and try-reload-or-restart, so a unit that is currently stopped is left stopped. On first activation that is the right behaviour, since the unit starts with the new secret anyway.
  • The calls are made with --no-block. sops-install-secrets.service runs before sysinit-reactivation.target with DefaultDependencies=no, and waiting on a regular service (which is ordered After=sysinit.target) would deadlock the transaction. So the restart is requested, not awaited.

Secrets needed to create users

Secrets marked neededForUsers are installed into /run/secrets-for-users before users are created, which is how you can source a user's password hash from sops:

1
2
3
4
5
6
7
8
9
{ config, ... }:
{
  sops.secrets.alice-password.neededForUsers = true;

  users.users.alice = {
    isNormalUser = true;
    hashedPasswordFile = config.sops.secrets.alice-password.path;
  };
}

This requires services.userborn.enable (the default), since userborn is what creates the users.

Secret file ownership

Secrets are written into a ramfs at /run/secrets.d, owned by root and by the keys group where it exists. Debian and Ubuntu have no keys group, so sops-install-secrets falls back to nogroup. This affects the mount point only; individual secrets still default to 0400 root-owned, and sops.secrets.<name>.owner/group work as usual.

Limitations

  • sops.useSystemdActivation cannot be disabled: the activation-script path it falls back to does not exist in System Manager. Setting it to false fails evaluation with an explanatory error.
  • Secrets live in /run and are re-installed on every boot and every activation. A machine that cannot reach its decryption key will fail sops-install-secrets.service rather than start services with stale secrets.

See also