Sops Nix

Updated: September 28, 2024

Secrets management for NixOS

Better than Agenix because it works with Home Manager.


Table of Contents



All keys in .sops.yaml are the pub age keys.
The keys.txt file holds the corresponding private age keys.
Each system needs the keys.txt placed at ~/.config/sops/age/keys.txt
First we need to setup .sops.yaml and then secrets.yaml.
After these are set we can work on sops.nix and installing into the flake.

SSH KEYS

ssh keys need to be converted to age keys to add to .sops.yaml
This is the file that allows the use of sops to encrypt and decrypt secrets.yaml

# create a folder that sops expects to exist when creating keys
mkdir -p ~/.config/sops/age

# create a new key from scratch
nix shell nixpkgs#age -c age-keygen -o ~/.config/sops/age/keys.txt

# create an age key from a private ssh key
nix run nixpkgs#ssh-to-age -- -private-key -i ~/.ssh/private > ~/.config/sops/age/keys.txt
nix-shell -p ssh-to-age --run 'cat ~/.ssh/id_ed25519.pub | ssh-to-age'

# print the public key of ~/.config/sops/age/keys.txt
nix shell nixpkgs#age -c age-keygen -y ~/.config/sops/age/keys.txt


Config

# create a config file at root of the flake and secrets folder
vim .sops.yaml

├── flake.nix
├── .sops.yaml
├── secrets/
│   ├── secrets.yaml
# .sops.yaml
# nix-shell -p ssh-to-age --run 'cat ~/.ssh/id_ed25519.pub | ssh-to-age'
# sops updatekeys secrets/secrets.yaml if you add/remove any keys or if no sops installed:
# nix-shell -p sops --run "sops updatekeys secrets/secrets.yaml"
keys:
  - &users:
    - &megacron agea8a7dsf98a7sydf8as7ydf8as7ydfaos87ydfoa7sdyfoa8s7ydfouahsdfliuhsd      # public key user
  - &hosts:
    - &blackout agea8a7dsf98a7sydf8as7ydf8as7ydfaos87ydfoa7sdyfoa8s7ydfouahsdfliuhsd      # age formatted host key
    - &energon agea8a7dsf98a7sydf8as7ydf8as7ydfaos87ydfoa7sdyfoa8s7ydfouahsdfliuhsd       # age formatted host key
    - &galvatron agea8a7dsf98a7sydf8as7ydf8as7ydfaos87ydfoa7sdyfoa8s7ydfouahsdfliuhsd     # age formatted host key
    - &ratchet agea8a7dsf98a7sydf8as7ydf8as7ydfaos87ydfoa7sdyfoa8s7ydfouahsdfliuhsd       # age formatted host key

creation_rules:
  - path_regex: secrets/secrets.yaml$
    key_groups:
    - age:
      - *megacron
      - *hosts

Secrets

Prior to using sops command there must exist a secrets directory in the root of the flake.

# create/edit a secrets file at root of the flake
sops secrets.yaml

# if you only have nix installed use nix shell
nix-shell -p sops --run "sops secrets/secrets.yaml"
# This is a comment
example_key: example_value
example_array:
  - example_value1
  - example_value2
example_number: 1234.5678
example_boolean:
  - true
  - false

User Passwords

Sops must run after NixOS creates users so it is not possible to set with
hashedPasswordFile.

# hash a password
echo "password" | mkpasswd -s

# configuration.nix
{ config, ... }:
{
  sops.secrets.megacron-password.neededForUsers = true;

  users.users.megacron = {
    isNormalUser = true;
    hashedPasswordFile = config.sops.secrets.megacron-password.path;
  };
}
# secrets.yaml
private_keys:
  megacron: |   # key must be same level of indentation as the pipe
            -----BEGIN PRIVATE KEY-----
            asfdasdfasdfioasdfoiasjdfoasidjfaosijdfasidjf
            -----END PRIVATE KEY-----
megacronPW: passwordwehashed  # mkpasswd -s
smtp-password: asfdasdfasdfioasdfoiasjdfoasidjfaosijdfasidjf
megacron-password: asfdasdfasdfioasdfoiasjdfoasidjfaosijdfasidjf



Nix Install

We will go into making a sops.nix module at the end.
This is just to understand the elements invovled on how sops works.

# add as an input to flake (follows line optional)
sops-nix.url = "github:Mic92/sops-nix";
sops-nix.inputs.nixpkgs.follows = "nixpkgs";

# then also add to nixosConfigurations
specialArgs = { inherit inputs; };  <-This
modules = [ ./configuration.nix ];

# open configuration.nix and add to imports
imports = [
  inputs.sops-nix.nixosModules.sops
];

sops = {
  defaultSopsFile = ./secrets/secrets.yaml;
  defaultSopsFormat = "yaml";
  age.keyFile = "home/user/.config/sops/age/keys.txt";

  # how to place secret values
  secrets.some-key = { };
  secrets."dir/subdir/secret" = { };

  # add another owner to your keys
  owner = "bob"
  owner = config.users.users.bob.name;
};

Now lets say we want to access and reveal a value

# this would cat out the value in plain text
$(cat ${config.sops.secrets."dir/subdir/secret".path})

sops.nix

{ inputs, config, ... }:

{
  imports = [
    inputs.sops-nix.nixosModules.sops
  ];

  sops = {
    defaultSopsFile = "secrets.yaml";
    validateSopsFiles = false;

    age = {
      sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];  # auto import host ssh keys as age keys
      keyFile = "/var/lib/sops/age/key.txt";  # expected age key to already be on filesystem
      generateKey = true;  # generate a new age key if one does not exist
    };

    # secrets will be output to /run/secrets
    # e.g. /run/secrets/msmtp-password
    # secrets required for user creation are handled in respective ./users/<username>.nix files
    # because they will be output to /run/secrets-for-users and only when the user is assigned to a host.
    secrets = {
      megacronPW = { };
      megacronPW.neededForUsers = true;

      msmtp-host = { };
      msmtp-address = { };
      msmtp-password = { };

      # extract to default pam-u2f authfile location for passwordless sudo. see ../optional/yubikey
      "yubico/u2f_keys" = {
        path = "/home/ta/.config/Yubico/u2f_keys";
      };
    };
  };
}
# configuration.nix
users.mutableUsers = false;     # allow pw to be set during build process
users.users.username = {
  hashedPasswordFile = config.sops.secrets.username.path;
  openssh.authorizedKeys = [
    (builtins.readFile ./secrets/id_blackout.pub)
  ];
};

Troubleshooting

sometimes rebuild may fail to start sops service.
Try runnning command with a --refresh flag
If that fails, then try

# afterward run without --refresh
systemctl --user reset-failed