Skip to content
IT related

SOPS-Nix

Updated 5 min read

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-nix is 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., age or GPG keys). This architecture allows multiple users and systems to decrypt the same file using their own unique keys.
  • Activation-Time Decryption: sops-nix decrypts secrets during system activation (e.g., when you run nixos-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. age is 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.

  1. Generate Your Personal "User" Key: This key proves your identity when editing secrets. You can convert your existing personal SSH key (ed25519 is recommended) into the age format.

    • 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 age private key, and saves it to ~/.config/sops/age/keys.txt. The sops CLI 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.
      age-keygen -y ~/.config/sops/age/keys.txt
      
      This will output a string starting with age1.... Copy this public key.
  2. 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 age public key format. Copy the resulting age1... string.
  3. Backup Your Personal Key: The private key stored in ~/.config/sops/age/keys.txt is 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.yaml file 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, sops automatically 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"
    ];
  };

IT related RSS feed Published with Oblish

Keyboard shortcuts

/
Search
[
Previous page
]
Next page
?
Show this list
Esc
Close