When setting up a NixOS system, we often want to store secrets in environment variables for various tools to use. For example, you might want to use the llm CLI tool, which requires setting the OPENAI_API_KEY
environment variable.
However, if you're using declarative configuration files or flakes to set up your NixOS system (which is a great practice!), you can't simply update those files with the secret. That would be insecure, especially if you're tracking your configurations in Git and pushing to a public repository.
Enter SOPS
To manage secrets on machines, we can use a tool called SOPS (Secrets OPerationS). SOPS allows you to manage secrets by encrypting them when storing them in a file. You use the tool to edit the secrets, and when you save, it writes them in an encrypted manner.
In NixOS, we have sops-nix, which helps manage SOPS-based secrets using configuration. Here's how it works: when SOPS secrets are loaded from a file at runtime, each secret is stored in a different file. This means at runtime, you can read from those files to get the secret and export it as an environment variable.
Setting Up SOPS with Age
We'll use Age as the encryption backend for SOPS.
Install Age and SOPS
In your home manager where you manage packages make sure to add age
and sops
and rebuilt to install them.
Generate an Age Key
First, create a directory for SOPS configuration and generate an Age key pair:
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
The public key is derived from the private key:
age-keygen -y ~/.config/sops/age/keys.txt
Copy the output public key; you'll need it to encrypt your secrets.
Create the .sops.yaml
Configuration File
Create a .sops.yaml
file in your project's root directory to define encryption rules and specify the public key:
keys:
- &host_hostname <YOUR PUBLIC KEY>
creation_rules:
- path_regex: secrets.yaml$
key_groups:
- age:
- *host_hostname
Replace <YOUR_PUBLIC_KEY>
with the public key you obtained earlier. And replace hostname
with your machine host-name. This configuration tells SOPS to use Age for encrypting any file named secrets.yaml
using the specified public key.
Create and Encrypt the Secrets File
Use nix-shell
with SOPS available to run creating the secrets.yaml
file. Make sure to run it from the same directory where .sops.yaml
exist. It will open the editor that is configured using EDITOR
in your shell.
nix-shell -p sops --run "sops secrets.yaml"
Add your secrets to the file:
openai_api_key: 'YOUR_OPENAI_API_KEY'
Save and exit the editor. SOPS will encrypt the file upon saving, and secrets.yaml
will now contain encrypted data along with other metadata.
Integrate SOPS with NixOS Using sops-nix
Update Your flake.nix
Add sops-nix
to your Nix flakes:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
sops-nix.url = "github:Mic92/sops-nix";
# other inputs...
};
outputs = { self, nixpkgs, sops-nix, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in
{
nixosConfigurations.mySystem = pkgs.lib.nixosSystem {
inherit system;
modules = [
./configuration.nix
sops-nix.nixosModules.sops
# other modules...
home-manager.nixosModules.home-manager
{
# other config...
home-manager.sharedModules = [
sops-nix.homeManagerModules.sops
];
}
];
};
};
}
Adding sops-nix
to the Inputs:
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
sops-nix.url = "github:Mic92/sops-nix";
# other inputs...
};
This line adds the sops-nix
input, fetching the sops-nix
repository from GitHub. It provides the necessary modules to integrate SOPS (Secrets OPerationS) into your NixOS system.
Including the SOPS NixOS Module:
modules = [
./configuration.nix
sops-nix.nixosModules.sops
# other modules...
];
By adding sops-nix.nixosModules.sops
to the modules
list, you're integrating the SOPS module into your NixOS system configuration. This enables system-wide management of encrypted secrets.
Integrating SOPS with Home Manager:
home-manager.nixosModules.home-manager
{
# other config...
home-manager.sharedModules = [
sops-nix.homeManagerModules.sops
];
}
This section includes the SOPS module in Home Manager's shared modules by adding sops-nix.homeManagerModules.sops
. It allows you to manage user-specific secrets through SOPS within your home environment configurations.
Configure SOPS in home.nix
In your NixOS configuration, set up SOPS:
{
sops = {
age.keyFile = "/home/<your username>/.config/sops/age/keys.txt"; # must have no password!
defaultSopsFile = ./secrets.yaml;
defaultSymlinkPath = "/run/user/1000/secrets";
defaultSecretsMountPoint = "/run/user/1000/secrets.d";
secrets.openai_api_key = {
# sopsFile = ./secrets.yml.enc; # optionally define per-secret files
path = "${config.sops.defaultSymlinkPath}/openai_api_key";
};
};
}
This configuration tells NixOS to use the encrypted secrets.yaml
file and makes the openai_api_key
available at the specified path. Whenever you add a new secret, you should update the config here with that secret info as well.
Accessing the Secret as an Environment Variable
In your config you can use $(cat ${config.sops.secrets.openai_api_key.path})
to get the secret. For example I configure my zsh file to export the OPENAI_API_KEY.
programs.zsh = {
initExtra = ''
# other config...
export OPENAI_API_KEY=$(cat ${config.sops.secrets.openai_api_key.path})
'';
};
Reload Configuration
After updating your configurations, rebuild your NixOS system:
sudo nixos-rebuild switch
Now, your OPENAI_API_KEY
is securely stored and accessible as an environment variable without exposing the key in your configuration files.
Conclusion
By integrating SOPS with NixOS, you maintain a declarative and secure configuration while managing sensitive secrets. This setup ensures that API keys and other confidential data remain encrypted in your repositories and are only decrypted on your local machine during runtime.
Remember to always be cautious when dealing with secrets and never commit unencrypted secrets to your repository!