DEV Community

Noor Latif
Noor Latif

Posted on

sops-nix secrets with NixOS using home-manager, the git-safe way

How to git track sensitive secrets without using env vars?

When you track your NixOS configuration in git, or any config for that matter, your entire system is version-controlled and reproducible. Any API keys or passwords in those files, however, would be pushed to your repo in plaintext for anyone to see. You can't just .gitignore them either, because NixOS needs those values at build/activation time. I use NixOS because it allowed me to have a code defined laptop environment that I can easily bring with me. I was tired of my Linux distros breaking, and I wasn't happy with Fedora Atomics way of doing things with rpm-ostree and RedHat image bootc feature.

sops-nix solves this: you encrypt secrets with SOPS and commit the encrypted files to git. At system activation, sops-nix decrypts them using keys that only exist on your machine (never in the repo). Your dotfiles stay fully reproducible and pushable, with zero secret leakage.

The first secret I needed to manage is EXA_API_KEY for Exa.ai, which powers web research and crawling capabilities through an MCP server.

This guide covers a simple single-machine setup using age keys derived from SSH keys, based on Michael Stapelberg's approach. But with some fixes to outdated methods.

Preparation: Add Trusted User (optional, recommended)

To avoid build warnings, add yourself as trusted user in configuration.nix (optional):

nix.settings.trusted-users = [ "@wheel" "noor" ];
Enter fullscreen mode Exit fullscreen mode

Then rebuild:

sudo nixos-rebuild switch
Enter fullscreen mode Exit fullscreen mode

This lets nix run use additional substituters (binary caches) without warnings. Speeds up the next steps and future ones too.

Step 1-3: Create age key and get both recipients

Prepare:

mkdir -p ~/.config/sops/age/
Enter fullscreen mode Exit fullscreen mode

Enter a nix shell with all needed tools:

nix shell nixpkgs#ssh-to-age nixpkgs#age
Enter fullscreen mode Exit fullscreen mode

Inside the shell:

Create an age identity file derived from your SSH private key.

If your SSH key is passphrase-protected, ssh-to-age will prompt you for it.

ssh-to-age -private-key -i ~/.ssh/id_ed25519 -o ~/.config/sops/age/keys.txt
Enter fullscreen mode Exit fullscreen mode

Lock down permissions (recommended):

chmod 600 ~/.config/sops/age/keys.txt
Enter fullscreen mode Exit fullscreen mode

Get your personal age recipient (copy the output, e.g., age1xxxxx...)

age-keygen -y ~/.config/sops/age/keys.txt
Enter fullscreen mode Exit fullscreen mode

Check if system SSH host keys exist

ls /etc/ssh/ssh_host_ed25519_key
Enter fullscreen mode Exit fullscreen mode
  • If not found, generate them: sudo ssh-keygen -A

Get your system's SSH host key recipient (copy the output, e.g., age1yyyyy...)

cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age
Enter fullscreen mode Exit fullscreen mode

Exit the shell

exit
Enter fullscreen mode Exit fullscreen mode

You'll need both recipients for .sops.yaml in the next step.

Step 4: Create .sops.yaml in nixos-dotfiles root

Create the file at ~/nixos-dotfiles/.sops.yaml:

keys:
  - &admin age1xxxxx...        # Your personal age recipient
  - &system age1yyyyy...       # Your system's SSH host key

creation_rules:
  - path_regex: secrets/[^/]+\.yaml$
    key_groups:
      - age:
          - *admin
          - *system
Enter fullscreen mode Exit fullscreen mode

Why both keys?

  • Your personal key (&admin): Lets you decrypt and edit secrets as a regular user, no sudo or repeated SSH passphrase needed
  • System SSH host key (&system): Lets the system decrypt secrets at boot automatically and make them available in /run/secrets/ without your intervention

With both:

  • System boots and decrypts secrets automatically (no passphrase needed)
  • You can edit secrets locally without root
  • Single secrets file works for both use cases
  • Most flexible and practical approach

Caveat: If you ever rotate/regenerate your SSH host key (/etc/ssh/ssh_host_ed25519_key), you must re-encrypt your secrets to the new host recipient, otherwise boot-time decryption will fail.

Step 5: Create the secrets file at ~/nixos-dotfiles/secrets/secrets.yaml

Create the secrets directory and open the encrypted editor

cd ~/nixos-dotfiles
mkdir -p secrets
nix run nixpkgs#sops -- secrets/secrets.yaml
Enter fullscreen mode Exit fullscreen mode

When the editor opens, add this content:

EXA_API_KEY: "your_actual_api_key_here"
Enter fullscreen mode Exit fullscreen mode

Save and exit. File is now encrypted.

Verify it's encrypted:

cat secrets/secrets.yaml
Enter fullscreen mode Exit fullscreen mode

You'll see encrypted gibberish (values start with ENC[AES256_GCM...). This proves the secret is encrypted.

Only you can decrypt it because you have the private key at ~/.config/sops/age/keys.txt. Without it, the file is useless to anyone else.

To edit or view it again:

nix run nixpkgs#sops -- secrets/secrets.yaml
Enter fullscreen mode Exit fullscreen mode

Only works because sops finds your private key and decrypts it automatically.

Step 6: Add sops-nix to flake.nix

Add sops-nix to inputs and modules:

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    home-manager.url = "github:nix-community/home-manager";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
    sops-nix = {
      url = "github:Mic92/sops-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, home-manager, sops-nix }:
  let
    system = "x86_64-linux";
  in {
    nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
      inherit system;
      modules = [
        sops-nix.nixosModules.sops
        ./configuration.nix
        home-manager.nixosModules.home-manager
        {
          home-manager.useGlobalPkgs = true;
          home-manager.useUserPackages = true;
          home-manager.users.noor = {
            imports = [
              sops-nix.homeManagerModules.sops
              ./home.nix
            ];
          };
        }
      ];
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

Important: Include sops-nix.homeManagerModules.sops in home-manager imports so the sops options are available in home.nix.

Step 7: Configure sops-nix in home.nix

In ~/nixos-dotfiles/home.nix, add:

{ config, ... }:

{
  # sops-nix secret management
  # Decrypt secrets and expose as environment variables
  sops.age.keyFile = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
  sops.defaultSopsFile = ./secrets/secrets.yaml;
  sops.secrets.EXA_API_KEY = { };

  home.sessionVariables = {
    EXA_API_KEY = "${config.sops.secrets.EXA_API_KEY.path}";
  };
}
Enter fullscreen mode Exit fullscreen mode

Note: EXA_API_KEY will contain the path to the decrypted secret file (e.g., /run/user/1000/secrets/EXA_API_KEY), not the key value itself. Use cat $EXA_API_KEY to read the actual key. This is how sops-nix works: secrets are files, and programs that support reading secrets from file paths will work seamlessly.

If a program expects the raw key value in EXA_API_KEY (not a file path), you’ll need to either (a) configure that program to read from a file (some support *_FILE patterns), or (b) wrap/launch it in a way that reads the file contents into an environment variable.

Important: After rebuilding, close all existing terminal windows. The environment variables won't update in shells that were open before the rebuild. Open a fresh terminal to get the new variables.

Step 7b: Enable sops in configuration.nix

Also add this in ~/nixos-dotfiles/configuration.nix:

{ ... }:

{
  sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
}
Enter fullscreen mode Exit fullscreen mode

Why? This is a system-level setting that tells the NixOS sops module which SSH key to use for decryption. It must go in configuration.nix (NixOS-level), not home.nix. The home-manager sops module uses its own sops.age.keyFile (set in Step 7). This system-level setting is needed so the system's SSH host key can also decrypt the secrets, enabling decryption at boot without user interaction and supporting future system-level secrets if you add any.

Step 8: Rebuild NixOS

sudo nixos-rebuild switch --flake ~/nixos-dotfiles#nixos
Enter fullscreen mode Exit fullscreen mode

Step 9: Reboot (or restart home-manager service)

Option A: Reboot (simplest)

sudo reboot
Enter fullscreen mode Exit fullscreen mode

Option B: Restart the service. Don't remember this working for me though.

systemctl --user daemon-reload
systemctl --user restart sops-nix.service
Enter fullscreen mode Exit fullscreen mode

(Due to a known home-manager issue, the service may not auto-restart during nixos-rebuild switch if it wasn't previously active.)

Step 10: Test it works

Open a fresh terminal and verify:

echo $EXA_API_KEY
# Should print the path, e.g., /run/user/1000/secrets/EXA_API_KEY

cat $EXA_API_KEY
# Should print your actual API key value
Enter fullscreen mode Exit fullscreen mode

Editing secrets later

cd ~/nixos-dotfiles
nix run nixpkgs#sops -- secrets/secrets.yaml
Enter fullscreen mode Exit fullscreen mode

Edit, save, commit, rebuild.

Git workflow

Commit everything:

git add flake.nix configuration.nix home.nix .sops.yaml secrets/secrets.yaml
git commit -m "Add EXA_API_KEY secret with sops-nix"
git push
Enter fullscreen mode Exit fullscreen mode

Your age private key (~/.config/sops/age/keys.txt) is never committed. It lives outside the repo. The encrypted secrets file (secrets/secrets.yaml) is safe to commit since it's useless without the private key.


Summary

We set up sops-nix so API keys can live in our git-tracked NixOS dotfiles without leaking:

  1. Derived an age key from our existing SSH key (ssh-to-age), stored at ~/.config/sops/age/keys.txt (never committed)
  2. Configured .sops.yaml with two recipients: personal age key (for editing) and system SSH host key (for boot-time decryption)
  3. Created an encrypted secrets file (secrets/secrets.yaml) using sops, committed to git and only readable with the private key
  4. Wired sops-nix into the flake: NixOS module in configuration.nix, Home Manager module in home.nix
  5. Exposed the secret as an env var ($EXA_API_KEY) pointing to the decrypted file path at /run/user/1000/secrets/EXA_API_KEY

The encrypted file is safe to push. The private key never leaves the machine. (In a feature article, I'll show to to leverage sops in a kubernetes cluster inside yaml files.)

Top comments (0)