Skip to content

Padraic-O-Mhuiris/secrets.nix

Repository files navigation

secrets.nix

Beta: This project is in active development. APIs may change.

Declarative SOPS secrets management for Nix flakes.

Overview

secrets.nix provides a pure Nix approach to managing encrypted secrets using SOPS and age. Each secret is:

  • Declared in your flake with explicit recipients
  • Stored as a single encrypted file (one file per secret)
  • Managed through generated shell scripts with full --help documentation

No .sops.yaml file needed - configuration is derived from your Nix expressions.

Quick Start

# flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    secrets-nix.url = "github:your/secrets.nix";
  };

  outputs = { nixpkgs, secrets-nix, ... }: let
    inherit (secrets-nix) mkSecrets mkSecretsPackages;

    recipients = {
      alice = {
        key = "age1abc...";  # alice's public key
      };
      bob = {
        key = "age1xyz...";  # bob's public key
      };
      server1 = {
        key = "age1srv...";  # server's public key
        decryptPkg = pkgs: pkgs.writeShellScriptBin "get-server1-key" ''
          ${pkgs.ssh-to-age}/bin/ssh-to-age -private-key -i /etc/ssh/ssh_host_ed25519_key
        '';
      };
    };

    secrets = mkSecrets {
      api-key = {
        dir = ./secrets;
        inherit recipients;
      };
      db-password = {
        dir = ./secrets;
        inherit recipients;
        format = "bin";  # default
      };
      service-account = {
        dir = ./secrets;
        inherit recipients;
        format = "json";
      };
    };
  in {
    packages.x86_64-linux = let
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
      secretsPkgs = mkSecretsPackages secrets pkgs;
    in {
      inherit secretsPkgs;
    };
  };
}

Operations

encrypt - Encrypt content to a secret file

# From file (secure - content not in shell history)
nix run .#secrets.api-key.encrypt -- --input <(pass show my-api-key)

# From a plaintext file
nix run .#secrets.api-key.encrypt -- --input ./plaintext.txt

# Override output location
nix run .#secrets.api-key.encrypt -- --input ./secret.txt --output ./other-dir/

# See all options
nix run .#secrets.api-key.encrypt -- --help

edit - Create or modify a secret interactively

# Create new secret (opens empty $EDITOR)
nix run .#secrets.new-secret.edit

# Edit existing secret (decrypts, opens $EDITOR, re-encrypts)
nix run .#secrets.api-key.edit.recipient.alice

# See all options
nix run .#secrets.api-key.edit -- --help

decrypt - Decrypt a secret

# To stdout (default)
nix run .#secrets.api-key.decrypt.recipient.alice

# To file
nix run .#secrets.api-key.decrypt.recipient.alice -- --output ./plaintext.txt

# With runtime key override
nix run .#secrets.api-key.decrypt -- --sopsAgeKeyCmd "pass show age/alice"
nix run .#secrets.api-key.decrypt -- --sopsAgeKeyFile ~/.config/sops/age/keys.txt

# Pipe to other commands
nix run .#secrets.service-account.decrypt.recipient.alice | jq .field

# See all options
nix run .#secrets.api-key.decrypt -- --help

rotate - Rotate the data encryption key

# Rotate data key (content unchanged)
nix run .#secrets.api-key.rotate.recipient.alice

rekey - Update recipients

# Update recipients to match current config (after adding/removing recipients)
nix run .#secrets.api-key.rekey.recipient.alice

Recipient Configuration

Each recipient needs a public key. Optionally, provide a decryptPkg function to enable decrypt.recipient.<name>:

recipients = {
  # Developer with decrypt capability
  alice = {
    key = "age1...";
    decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
      pass show age/alice
    '';
  };

  # Deploy target (encrypt-only, no decryptPkg)
  production = {
    key = "age1...";
  };
};

Formats

Supported secret formats:

Format Extension Use case
bin (none) Binary/text secrets (default)
json .json Structured JSON data
yaml .yaml Structured YAML data
env .env Environment files

Key Configuration

There are multiple ways to provide the age secret key for decryption, at both build time (Nix evaluation) and runtime (shell execution).

Build-time Configuration

Configure the key source when building packages in your flake:

1. Per-recipient packages (recommended)

Define decryptPkg in your recipients to get decrypt.recipient.<name>:

recipients = {
  alice = {
    key = "age1...";
    decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
      pass show age/alice
    '';
  };
};

# Then use:
packages.decrypt-secret = secrets.api-key.decrypt.recipient.alice;

2. Builder pattern

Chain builder methods for one-off configurations:

{
  # String command (executed at runtime)
  my-decrypt = secrets.api-key.decrypt.withSopsAgeKeyCmd "pass show age/key";

  # Pre-built package (must output key to stdout)
  my-decrypt = secrets.api-key.decrypt.withSopsAgeKeyCmdPkg myKeyPkg;

  # Build function (pkgs -> derivation)
  my-decrypt = secrets.api-key.decrypt.buildSopsAgeKeyCmdPkg (pkgs:
    pkgs.writeShellScriptBin "get-key" ''
      ${pkgs.ssh-to-age}/bin/ssh-to-age -private-key -i ~/.ssh/id_ed25519
    ''
  );
}

Runtime Configuration

Override or provide key configuration when running the command:

1. Command-line flags

# Command that outputs the key (most secure)
nix run .#secrets.api-key.decrypt -- --sopsAgeKeyCmd "pass show age/key"

# Path to key file
nix run .#secrets.api-key.decrypt -- --sopsAgeKeyFile ~/.config/sops/age/keys.txt

# Direct key value (visible in ps - use only for testing)
nix run .#secrets.api-key.decrypt -- --sopsAgeKey "AGE-SECRET-KEY-1..."

2. Environment variables

# Command that outputs the key
export SOPS_AGE_KEY_CMD="pass show age/key"
nix run .#secrets.api-key.decrypt

# Path to key file
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
nix run .#secrets.api-key.decrypt

# Direct key value
export SOPS_AGE_KEY="AGE-SECRET-KEY-1..."
nix run .#secrets.api-key.decrypt

3. Using direnv

Add to .envrc for automatic key configuration per-project:

# .envrc
export SOPS_AGE_KEY_CMD="pass show age/myproject"

Key Resolution Order

When decrypting, keys are resolved in this order (first match wins):

  1. --sopsAgeKey flag
  2. --sopsAgeKeyFile flag
  3. --sopsAgeKeyCmd flag
  4. SOPS_AGE_KEY environment variable
  5. SOPS_AGE_KEY_FILE environment variable
  6. SOPS_AGE_KEY_CMD environment variable
  7. Build-time configured key (from decryptPkg or builder pattern)

Common Key Sources

# Password store (pass)
decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
  ${pkgs.pass}/bin/pass show age/mykey
'';

# 1Password CLI
decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
  ${pkgs._1password}/bin/op read "op://vault/age-key/secret"
'';

# SSH key via ssh-to-age
decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
  ${pkgs.ssh-to-age}/bin/ssh-to-age -private-key -i ~/.ssh/id_ed25519
'';

# HashiCorp Vault
decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
  ${pkgs.vault}/bin/vault kv get -field=key secret/age
'';

# AWS Secrets Manager
decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
  ${pkgs.awscli2}/bin/aws secretsmanager get-secret-value \
    --secret-id age-key --query SecretString --output text
'';

# Bitwarden CLI
decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
  ${pkgs.bitwarden-cli}/bin/bw get password age-key
'';

# macOS Keychain
decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
  security find-generic-password -s "age-key" -w
'';

# Plain file (least secure, but simple)
decryptPkg = pkgs: pkgs.writeShellScriptBin "get-key" ''
  cat ~/.config/sops/age/keys.txt
'';

Project Structure

your-project/
├── flake.nix          # Secret definitions
└── secrets/
    ├── api-key        # Encrypted secret (binary format)
    ├── db-password    # Encrypted secret
    └── service-account.json  # Encrypted secret (json format)

DevShell Integration

Include secret management tools in your development shell:

{
  devShells.default = pkgs.mkShell {
    packages = [
      pkgs.sops
      pkgs.age

      # Include specific decrypt commands
      secretsPkgs.api-key.decrypt.recipient.alice
      secretsPkgs.db-password.decrypt.recipient.alice

      # Or create a wrapper with all operations for a recipient
      (pkgs.symlinkJoin {
        name = "secrets-alice";
        paths = [
          secretsPkgs.api-key.decrypt.recipient.alice
          secretsPkgs.api-key.edit.recipient.alice
          secretsPkgs.api-key.rotate.recipient.alice
          secretsPkgs.db-password.decrypt.recipient.alice
        ];
      })
    ];

    # Or configure via environment
    shellHook = ''
      export SOPS_AGE_KEY_CMD="pass show age/alice"
    '';
  };
}

Then in your shell:

# Decrypt directly
decrypt-api-key

# Use in scripts
API_KEY=$(decrypt-api-key)
curl -H "Authorization: Bearer $API_KEY" https://api.example.com

# Edit a secret
edit-api-key

Future Work

  • Additional SOPS key types:
    • GPG keys
    • AWS KMS
    • GCP KMS
    • Azure Key Vault
    • HashiCorp Vault Transit
  • sopsnix/agenix integration
  • flake-parts module

Links

About

A flake-parts module for secrets management

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages