sops-nix secrets with NixOS using home-manager, the git-safe way
Source: Dev.to
When you version‑control your NixOS configuration (or any dotfiles) the whole system becomes reproducible.
However, any API keys or passwords that live in those files would be stored in plain text in the repository.
- You can’t simply
.gitignorethem – NixOS needs the values at build/activation time. - You also don’t want to expose them as environment variables.
Solution: Use sops‑nix – encrypt secrets with SOPS and commit the encrypted files.
At activation, sops‑nix decrypts them with keys that only exist on your machine (never in the repo).
Your dotfiles stay fully reproducible, pushable, and leak‑free.
Overview
- First secret to manage:
EXA_API_KEY(for Exa.ai). - This guide shows a single‑machine setup using age keys derived from your SSH keys (based on Michael Stapelberg’s approach, with fixes).
- Optional: add yourself as a trusted user to silence build warnings.
# configuration.nix
nix.settings.trusted-users = [ "@wheel" "noor" ];
Rebuild once:
sudo nixos-rebuild switch
1️⃣ Prepare the Age Key Store
# Create the directory where sops will look for age keys
mkdir -p ~/.config/sops/age/
Enter a temporary Nix shell that contains the required tools:
nix shell nixpkgs#ssh-to-age nixpkgs#age
1.1 Create an age identity from your SSH private key
# If your SSH key is passphrase‑protected, you’ll be prompted for it.
ssh-to-age -private-key -i ~/.ssh/id_ed25519 -o ~/.config/sops/age/keys.txt
Lock down the permissions (highly recommended):
chmod 600 ~/.config/sops/age/keys.txt
1.2 Get your personal age recipient
age-keygen -y ~/.config/sops/age/keys.txt
# → prints something like: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Copy the printed value – you’ll need it later.
1.3 Ensure the system SSH host key exists
ls /etc/ssh/ssh_host_ed25519_key # should exist
If it does not exist, generate all host keys:
sudo ssh-keygen -A
1.4 Get the system age recipient (derived from the host key)
cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age
# → prints something like: age1yyyyyyyyyyyyyyyyyyyyyyyyyyyy
Copy this value as well.
Exit the temporary shell:
exit
You now have two recipients:
| Recipient | Value (example) |
|---|---|
admin (personal) | age1xxxxx… |
system (host) | age1yyyyy… |
2️⃣ Create the .sops.yaml Configuration
Create the file at the root of your dotfiles repository (e.g. ~/nixos-dotfiles/.sops.yaml):
# .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
What this does
- Personal key (
&admin) – lets you decrypt/edit secrets as a regular user (nosudo, no repeated SSH passphrase). - System key (
&system) – lets the system decrypt secrets automatically at boot and expose them under/run/secrets/.
⚠️ Caveat: If you ever rotate/regenerate the SSH host key (
/etc/ssh/ssh_host_ed25519_key), you must re‑encrypt all secrets with the new host recipient, otherwise boot‑time decryption will fail.
3️⃣ Create and Encrypt Your First Secret
cd ~/nixos-dotfiles
mkdir -p secrets
Open the file with SOPS (the nix run wrapper pulls in the correct version):
nix run nixpkgs#sops -- secrets/secrets.yaml
When the editor opens, add the secret:
EXA_API_KEY: "your_actual_api_key_here"
Save & quit. The file is now encrypted.
Verify the encryption:
cat secrets/secrets.yaml
# → you’ll see values starting with ENC[AES256_GCM,...]
Only you (with ~/.config/sops/age/keys.txt) can decrypt it.
To edit/view later:
nix run nixpkgs#sops -- secrets/secrets.yaml
4️⃣ Wire sops‑nix into Your NixOS Configuration
4.1 Add sops‑nix as a Flake Input
# flake.nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/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 # **Important:** `sops-nix.homeManagerModules.sops` must be imported so the `sops` options are visible inside `home.nix`.
];
};
};
}
4.2 Configure Secrets in home.nix
# home.nix
{ config, ... }:
{
# Tell sops‑nix where your age private key lives
sops.age.keyFile = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
# Default SOPS file for this user
sops.defaultSopsFile = ./secrets/secrets.yaml;
# Declare the secret we want to expose
sops.secrets.EXA_API_KEY = { };
# Export a session variable that points to the decrypted file
home.sessionVariables = {
EXA_API_KEY = "${config.sops.secrets.EXA_API_KEY.path}";
};
}
What you get
- At login,
EXA_API_KEYpoints to a temporary file under/run/user/<uid>/secrets/EXA_API_KEY. - The file contains the raw key value.
- To read it in a shell:
cat "$EXA_API_KEY".
Note:
sops‑nixstores secrets as files, not as raw environment variables. Programs that can read a secret from a file path work out‑of‑the‑box.
5️⃣ Using the Secret
# In a shell
echo "My key is: $(cat "$EXA_API_KEY")"
If a program expects the raw value directly in an environment variable, you can wrap it:
export EXA_API_KEY=$(cat "$EXA_API_KEY")
my-program --api-key "$EXA_API_KEY"
Recap
| Step | What you did |
|---|---|
| 1️⃣ | Generated an age identity from your SSH key and collected both personal & system recipients. |
| 2️⃣ | Created .sops.yaml that tells SOPS which recipients may encrypt/decrypt the files under secrets/. |
| 3️⃣ | Added EXA_API_KEY to secrets/secrets.yaml and encrypted it with SOPS. |
| 4️⃣ | Added sops‑nix to your flake, enabled the module for both NixOS and Home Manager, and pointed it at your key file and secret file. |
| 5️⃣ | Exported a session variable that points to the decrypted secret file, ready for use. |
Now your NixOS configuration (or any dotfiles repo) can be safely pushed to a public or shared Git remote without ever leaking secrets. 🎉
Adding an API Key with sops‑nix
1. Why the extra step?
API_KEY is not a file path. To make a program that expects a file work you have two options:
- Configure the program to read from a file (many programs support
*_FILEenv‑vars). - Wrap/launch the program so it reads the file contents into an environment variable.
After rebuilding, close all existing terminal windows – open shells won’t see the new variables. Open a fresh terminal to get the updated environment.
2. System‑level sops configuration
Add the following to ~/nixos-dotfiles/configuration.nix (note the outer { … }: wrapper required by NixOS modules):
{ ... }:
{
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
}
What this does
- Declares the system‑level SSH host key that the NixOS
sopsmodule will use for decryption. - Must live in
configuration.nix(NixOS level), not inhome.nix. - The Home‑Manager
sopsmodule uses its ownsops.age.keyFile(set in Step 4). - Enables decryption at boot without user interaction and allows future system‑level secrets.
3. Rebuild & apply
sudo nixos-rebuild switch --flake ~/nixos-dotfiles#nixos
Options after the rebuild
| Option | Command | Note |
|---|---|---|
| A – Reboot (simplest) | sudo reboot | Guarantees all services see the new env‑vars. |
| B – Restart the service | bash\nsystemctl --user daemon-reload\nsystemctl --user restart sops-nix.service\n | May not work if the service wasn’t previously active (home‑manager issue). |
4. Verify the secret
Open a new terminal and run:
echo $EXA_API_KEY
# → should print a path like /run/user/1000/secrets/EXA_API_KEY
cat $EXA_API_KEY
# → prints the actual API‑key value
5. Edit the encrypted secrets file
cd ~/nixos-dotfiles
nix run nixpkgs#sops -- secrets/secrets.yaml
Make your changes, then save, commit, and rebuild.
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
Important:
- Your age private key (
~/.config/sops/age/keys.txt) is never committed – it stays outside the repo.- The encrypted file (
secrets/secrets.yaml) is safe to commit because it’s useless without the private key.
6. What we did (summary)
- Generated an age key from the existing SSH host key (
ssh-to-age) and stored it at~/.config/sops/age/keys.txt(untracked). - Configured
.sops.yamlwith two recipients: personal age key (for editing) and system SSH host key (for boot‑time decryption). - Created an encrypted secrets file (
secrets/secrets.yaml) usingsops. - Wired
sops‑nixinto the flake: NixOS module inconfiguration.nixand Home‑Manager module inhome.nix. - Exposed the secret as an environment variable
$EXA_API_KEY, which points to the decrypted file at/run/user/1000/secrets/EXA_API_KEY.
The encrypted file can be pushed safely; the private key never leaves the machine. (In a future article I’ll show how to leverage sops in a Kubernetes cluster via YAML files.)