SOPS-Nix
On this page 15 sections
- Summary: Declarative Secret Management with sops-nix
- Core Concepts
- The sops-nix Workflow: A Step-by-Step Guide
- Step 1: Integrate into flake.nix
- Step 2: Generate and Manage Keys
- Step 3: Create the .sops.yaml Configuration
- Step 4: Create and Edit the Secrets File
- Step 5: Configure the sops-nix Module
- Step 6: Use the Secrets in Your Configuration
- Secret Definition Options
- Advanced Use Cases and Examples
- Templating Secrets into Configuration Files
- Automatic Service Restarts on Secret Change
- Home Manager Integration
- SOPS for Filesystems (NAS)
Summary: Declarative Secret Management with sops-nix
sops-nix is a NixOS module that provides atomic, declarative, and reproducible secret management by integrating Mozilla's SOPS (Secrets OPerationS) tool directly into the NixOS ecosystem. It allows you to safely commit encrypted secrets to your version-controlled configuration repository and have them securely decrypted and provisioned to services at system activation time.
Core Concepts
- SOPS (Secrets OPerationS): The underlying tool that
sops-nixis built upon. SOPS is an editor for encrypted files. It uniquely encrypts only the values within structured files (like YAML or JSON), leaving the keys and structure in plaintext. This makes changes easy to review in Git diffs. - Envelope Encryption: SOPS uses a robust method called envelope encryption. For each file, it generates a unique Data Encryption Key (DEK) to encrypt the secret values. This DEK is then itself encrypted by one or more "master keys" (e.g.,
ageor GPG keys). This architecture allows multiple users and systems to decrypt the same file using their own unique keys. - Activation-Time Decryption:
sops-nixdecrypts secrets during system activation (e.g., when you runnixos-rebuild switch). It creates individual files for each secret in a temporary, permission-controlled directory, typically/run/secrets. This ensures that plaintext secrets never end up in the world-readable Nix store. - Backend Support: While SOPS supports various encryption backends like GPG, AWS KMS, and Azure Key Vault, the most common and straightforward method for NixOS users is
age.ageis a modern, simple encryption tool that integrates seamlessly with existing SSH keys.
The sops-nix Workflow: A Step-by-Step Guide
Here is a complete walkthrough for integrating sops-nix into a NixOS configuration using flakes and the age backend.
Step 1: Integrate into flake.nix
First, add sops-nix as an input to your flake.nix and include its NixOS module in your system configuration.
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# Add sops-nix input
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
#... other inputs
};
outputs = { self, nixpkgs, sops-nix,... }: {
nixosConfigurations.your-hostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
# Import the sops-nix module
sops-nix.nixosModules.sops
];
};
};
}
**
Step 2: Generate and Manage Keys
You need two types of keys: one for yourself to edit secrets, and one for each host machine to decrypt secrets.
-
Generate Your Personal "User" Key: This key proves your identity when editing secrets. You can convert your existing personal SSH key (
ed25519is recommended) into theageformat.- Command:
nix-shell -p ssh-to-age --run "ssh-to-age -private-key -i ~/.ssh/id_ed25519 > ~/.config/sops/age/keys.txt" - Action: This command reads your SSH private key, converts it to an
ageprivate key, and saves it to~/.config/sops/age/keys.txt. ThesopsCLI automatically looks in this location for a key to decrypt files. - Get Public Key for Configuration: Extract the public part of this key, which you'll need for the next step.
This will output a string starting withage-keygen -y ~/.config/sops/age/keys.txtage1.... Copy this public key.
- Command:
-
Get the "Host" Key: This key allows the NixOS machine to decrypt secrets during activation.
- Command:
nix-shell -p ssh-to-age --run "cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age" - Action: This reads the system's public SSH host key and converts it to the
agepublic key format. Copy the resultingage1...string.
- Command:
-
Backup Your Personal Key: The private key stored in
~/.config/sops/age/keys.txtis your master key for editing. If you lose it, you will be permanently locked out of editing your secrets. It is critical to back up the entire contents of this file in a secure password manager like Bitwarden.
Step 3: Create the .sops.yaml Configuration
At the root of your Nix configuration repository, create a file named .sops.yaml. This file defines the encryption rules for sops.
#.sops.yaml
keys:
# This is your personal public key for editing.
- &user_key age1... # Paste your personal age public key here
# This is the host's public key for decryption.
- &host_key age1... # Paste the host's age public key here
creation_rules:
# This rule applies to any file named secrets.yaml
- path_regex: secrets.yaml$
key_groups:
- age:
- *user_key
- *host_key
**
Step 4: Create and Edit the Secrets File
You do not create a plaintext file and then encrypt it. Instead, you use the sops command to edit the file, and it handles encryption automatically upon saving.
- Command:
nix-shell -p sops --run "sops secrets.yaml" - Action: This opens a new (or existing)
secrets.yamlfile in your default command-line editor. The contents will be in plaintext. Add your secrets in a structured YAML format.# secrets.yaml user: hashedPassword: "your_password_hash_here" services: api_token: "your_super_secret_token" - When you save and exit the editor,
sopsautomatically encrypts the values and writes the result to disk.
Step 5: Configure the sops-nix Module
In your configuration.nix, define which secrets to use and how they should be handled.
# configuration.nix
{ config, pkgs,... }:
{
# SOPS Configuration
sops = {
# This tells sops-nix where to find the host's private key for decryption.
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
# Define the default secrets file to use.
defaultSopsFile =./secrets.yaml;
# Declare each secret you want to make available.
# The name corresponds to the YAML path.
secrets."user/hashedPassword" = {
# This option is crucial for user passwords. It ensures the secret is
# decrypted before the system manages user accounts.
neededForUsers = true;
};
secrets."services/api_token" = {
# Set ownership and permissions for the decrypted file.
owner = config.users.users.my-service-user.name;
mode = "0400"; # Read-only for the owner
};
};
#... rest of your configuration
}
**
Step 6: Use the Secrets in Your Configuration
Once declared, you can reference the path to the decrypted secret file anywhere in your NixOS modules using config.sops.secrets.<name>.path. The secret's value is never directly exposed in the Nix store.
-
Example: Setting a user password
users.users.myuser = { isNormalUser = true; # Use `hashedPasswordFile` to point to the decrypted secret path. hashedPasswordFile = config.sops.secrets."user/hashedPassword".path; }; -
Example: Providing a secret to a service
systemd.services.my-service = { serviceConfig.EnvironmentFile = config.sops.secrets."services/api_token".path; };
Secret Definition Options
When you define a secret under sops.secrets.<name>, you can specify several options to control its behavior:
| Option | Type | Description |
|---|---|---|
owner |
String | The user who will own the decrypted secret file. Defaults to root. It's best to use config.users.users.<name>.name to avoid typos. |
group |
String | The group that will own the decrypted secret file. Defaults to keys. It's best to use config.users.users.<name>.group. |
mode |
String | The file permissions for the decrypted secret file, in octal format (e.g., "0400" for read-only by owner). |
path |
String | The absolute path where the decrypted secret file will be placed. Defaults to /run/secrets/<name>. |
neededForUsers |
Boolean | If true, the secret is decrypted to /run/secrets-for-users very early in the boot process, before user accounts are created. This is essential for setting user passwords. |
restartUnits |
List of Strings | A list of systemd units (e.g., [ "nginx.service" ]) that will be automatically restarted if the secret's content changes. |
reloadUnits |
List of Strings | A list of systemd units that will be automatically reloaded if the secret's content changes. |
sopsFile |
Path | Overrides the sops.defaultSopsFile for this specific secret, allowing you to pull secrets from multiple source files. |
format |
String | Overrides the sops.defaultSopsFormat. Can be "yaml", "json", "ini", "dotenv", or "binary". |
key |
String | For YAML/JSON formats, specifies which key to extract. If set to "", the entire decrypted file is provided, not just a single value. |
Advanced Use Cases and Examples
Templating Secrets into Configuration Files
Some applications require secrets to be embedded directly in their configuration files and don't support reading them from a separate file. sops-nix provides a powerful templating feature for this.
-
secrets.yaml:app_database: password: "super-secret-db-password" -
configuration.nix:{ config, pkgs,... }: { sops.secrets."app_database/password" = {}; # Define a template for the application's config file sops.templates."my-app.conf" = { owner = "my-app-user"; content = '' # my-app.conf # The placeholder will be replaced with the actual secret value. database_password = "${config.sops.placeholder."app_database/password"}" ''; }; # Use the generated config file in the service services.my-app = { enable = true; user = "my-app-user"; configFile = config.sops.templates."my-app.conf".path; }; }
Automatic Service Restarts on Secret Change
A key feature of sops-nix is its ability to automatically restart or reload services when a secret is updated. This is invaluable for rotating credentials without manual intervention.
-
secrets.yaml:home-assistant: secrets_yaml: | # This is a multi-line string that will become the content of secrets.yaml ha_api_key: "new-rotated-key-12345" some_other_secret: "value" -
configuration.nix:{ config,... }: { sops.secrets."home-assistant/secrets_yaml" = { owner = "home-assistant"; # Tell sops-nix to restart the service when this secret changes restartUnits = [ "home-assistant.service" ]; }; services.home-assistant = { enable = true; #... other config config = { # Use the special!secret syntax to reference the sops-managed file secrets: "!include ${config.sops.secrets."home-assistant/secrets_yaml".path}"; }; }; }
Home Manager Integration
sops-nix provides a Home Manager module to manage user-level secrets, such as shell environment variables or application keys.
-
flake.nix:# In your nixosConfigurations output modules = [ #... sops-nix.nixosModules.sops home-manager.nixosModules.home-manager { home-manager.useGlobalPkgs = true; home-manager.useUserPackages = true; home-manager.users.your_user = { imports = [ sops-nix.homeManagerModules.sops ./home.nix ]; }; } ]; -
home.nix:{ config, pkgs,... }: { sops = { age.keyFile = "/home/your_user/.config/sops/age/keys.txt"; defaultSopsFile =./secrets.yaml; # These paths are often necessary to fix path resolution issues in Home Manager defaultSymlinkPath = "/run/user/1000/secrets"; defaultSecretsMountPoint = "/run/user/1000/secrets.d"; secrets.OPENAI_API_KEY = {}; }; # Export the secret as an environment variable in Zsh programs.zsh.initExtra = '' export OPENAI_API_KEY="$(cat ${config.sops.secrets.OPENAI_API_KEY.path})" ''; }
SOPS for Filesystems (NAS)
sops-nix can be used to provide the credenticals for a SMB NAS or similar. Here is what you need to do, this configuration takes place at the configuration.nix level not home.nix level because you need root to mount the filesystem.
First declare the secrets in the sops secrets file however you wish:
nas:
nas-1:
username: fullmetal
password: changeme
Then we need to make the sops configuration in configuration.nix:
# SOPS configuration
sops = {
secrets."nas/nas1/username" = {};
secrets."nas/nas1/password" = {};
templates."smb-credentials" = {
content = ''
username=${config.sops.placeholder."nas/nas1/username"}
password=${config.sops.placeholder."nas/nas1/password"}
'';
mode = "0600";
};
};
Now you need to actually add the file system (change the options as required, these are sensible defaults.):
fileSystems."/home/fullmetal/nas1" = {
device = "//<ip-or-hostname>/<smb-share-name>";
fsType = "cifs";
options = [
"credentials=${config.sops.templates."smb-credentials".path}"
"uid=fullmetal"
"gid=users"
"noauto"
"x-systemd.automount"
"x-systemd.after=sops-nix.service"
"x-systemd.idle-timeout=600"
"x-systemd.device-timeout=10"
];
};