Skip to content

Managing Secrets

NullString1 edited this page Jan 13, 2026 · 2 revisions

Managing Secrets

How to securely manage sensitive data in your NullOS configuration.

Overview

NullOS configuration is stored in Git, but some information should remain private:

  • Passwords
  • API keys
  • SSH keys
  • VPN credentials
  • Backup encryption keys

This guide covers strategies for keeping secrets out of version control.


Strategy 1: Separate Files (Gitignored)

Using variables.nix

The simplest approach - variables.nix is already gitignored:

# variables.nix (gitignored)
{
  username = "yourname";
  gitEmail = "private@email.com";
  resticRepository = "sftp:user@private-server.com:/backups";
  # Other private settings
}

Additional Secret Files

Create separate files for secrets:

# secrets.nix (add to .gitignore)
{
  apiKeys = {
    github = "ghp_xxxxxxxxxxxx";
    openai = "sk-xxxxxxxxxxxxxxx";
  };
  
  vpnCredentials = {
    mullvad = "1234567890123456";
  };
}

Import in configuration:

let
  secrets = import ./secrets.nix;
in
{
  # Use secrets here
  environment.variables.GITHUB_TOKEN = secrets.apiKeys.github;
}

.gitignore:

variables.nix
secrets.nix
*.secret.nix

Strategy 2: Environment Files

Store secrets in files outside the Nix store:

Password Files

# modules/services/backup.nix
services.restic.backups.nsdata = {
  repository = vars.resticRepository;
  passwordFile = "/etc/nixos/secrets/restic-password";
  # File is read at runtime, not stored in Nix store
};

Creating Secret Files

# Create secrets directory
sudo mkdir -p /etc/nixos/secrets
sudo chmod 700 /etc/nixos/secrets

# Add password
echo "my-secret-password" | sudo tee /etc/nixos/secrets/restic-password
sudo chmod 600 /etc/nixos/secrets/restic-password

Home Directory Secrets

For user-level secrets:

mkdir -p ~/.config/secrets
chmod 700 ~/.config/secrets

echo "token" > ~/.config/secrets/api-token
chmod 600 ~/.config/secrets/api-token

Use in home configuration:

programs.git.extraConfig = {
  github.user = builtins.readFile "/home/${username}/.config/secrets/github-user";
};

Strategy 3: Age Encryption (agenix)

For advanced secret management, use agenix.

Setup agenix

Add to flake.nix inputs:

inputs = {
  # Existing inputs...
  agenix = {
    url = "github:ryantm/agenix";
    inputs.nixpkgs.follows = "nixpkgs";
  };
};

Import module:

# In nixosConfiguration modules
imports = [
  inputs.agenix.nixosModules.default
];

Generate SSH Key for Encryption

ssh-keygen -t ed25519 -f ~/.ssh/agenix -C "agenix"

Create secrets.nix Manifest

# secrets/secrets.nix
let
  user1 = "ssh-ed25519 AAAAC3... user@machine1";
  user2 = "ssh-ed25519 AAAAC3... user@machine2";
  
  allUsers = [ user1 user2 ];
in
{
  "restic-password.age".publicKeys = allUsers;
  "vpn-config.age".publicKeys = allUsers;
  "api-keys.age".publicKeys = allUsers;
}

Encrypt Secrets

# Install agenix CLI
nix profile install github:ryantm/agenix

# Encrypt a secret
agenix -e restic-password.age

Use Encrypted Secrets

# In configuration
age.secrets.restic-password = {
  file = ./secrets/restic-password.age;
  owner = "root";
  mode = "600";
};

# Reference in services
services.restic.backups.nsdata = {
  passwordFile = config.age.secrets.restic-password.path;
};

Strategy 4: sops-nix

Alternative to agenix using SOPS.

Setup sops-nix

# flake.nix inputs
inputs.sops-nix = {
  url = "github:Mic92/sops-nix";
  inputs.nixpkgs.follows = "nixpkgs";
};

# Import module
imports = [
  inputs.sops-nix.nixosModules.sops
];

Create .sops.yaml

keys:
  - &admin age1xxxxxxxxxxxxxxxxxx

creation_rules:
  - path_regex: secrets/.*\.yaml$
    key_groups:
      - age:
          - *admin

Encrypt with SOPS

# Install sops
nix-shell -p sops

# Create secret file
sops secrets/passwords.yaml

YAML content:

restic_password: my-secret-password
github_token: ghp_xxxxxx

Use in Configuration

sops.secrets = {
  "restic_password" = {
    sopsFile = ./secrets/passwords.yaml;
  };
};

# Reference
services.restic.backups.nsdata = {
  passwordFile = config.sops.secrets.restic_password.path;
};

Managing SSH Keys

Generate SSH Keys

ssh-keygen -t ed25519 -C "your@email.com"

Deploy SSH Keys

Don't put in Nix config! Instead:

# Copy to new machine
ssh-copy-id user@newmachine

Or manually:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
cp id_ed25519* ~/.ssh/
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

SSH Config (Safe to Include)

Public SSH config is safe:

# home/ssh.nix
programs.ssh = {
  enable = true;
  
  matchBlocks = {
    "github.com" = {
      user = "git";
      identityFile = "~/.ssh/id_ed25519";
    };
    
    "myserver" = {
      hostname = "192.168.1.100";
      user = "admin";
      identityFile = "~/.ssh/id_ed25519";
    };
  };
};

VPN Credentials

Tailscale

Tailscale auth is handled via web login:

tailscale up
# Follow authentication prompt

No secrets in configuration needed!

Mullvad

Store account number in secret file:

echo "1234567890123456" | sudo tee /etc/nixos/secrets/mullvad-account

Use in script:

mullvad account login "$(cat /etc/nixos/secrets/mullvad-account)"

WireGuard

Store private key separately:

# modules/services/vpn.nix
networking.wireguard.interfaces.wg0 = {
  privateKeyFile = "/etc/nixos/secrets/wireguard-private.key";
  # Public config is safe
  peers = [
    {
      publicKey = "xxxx=";  # Safe to commit
      endpoint = "vpn.example.com:51820";
    }
  ];
};

Git Credentials

GitHub CLI

Authenticate interactively:

gh auth login

Stores token in ~/.config/gh/ (not in Nix config).

Git Credential Helper

Use system credential manager:

programs.git = {
  enable = true;
  extraConfig = {
    credential.helper = "store";  # or "cache"
  };
};

Credentials stored in ~/.git-credentials (gitignored).


API Keys & Tokens

Environment Variables

Store in shell profile (outside Nix):

# ~/.zshenv (not managed by Nix)
export OPENAI_API_KEY="sk-xxxxx"
export GITHUB_TOKEN="ghp_xxxxx"

Runtime Configuration

Load from files at runtime:

# home/default.nix
home.sessionVariables = {
  GITHUB_TOKEN = builtins.readFile /home/${username}/.config/secrets/github-token;
};

Application-Specific

Many apps have their own credential storage:

  • VSCode: Stores tokens in encrypted keyring
  • Docker: ~/.docker/config.json
  • AWS: ~/.aws/credentials

These don't need to be in Nix config.


Backup Encryption

Restic Password

Never commit the password!

services.restic.backups.nsdata = {
  repository = "sftp:user@server:/backup";
  
  # Read from file
  passwordFile = "/etc/nixos/secrets/restic-password";
  
  # Or use environment variable
  # environmentFile = "/etc/nixos/secrets/restic-env";
};

Creating Password File

sudo mkdir -p /etc/nixos/secrets
echo "my-strong-password" | sudo tee /etc/nixos/secrets/restic-password
sudo chmod 600 /etc/nixos/secrets/restic-password

Best Practices

1. Never Commit Secrets

Safe to commit:

  • Usernames (if not sensitive)
  • Hostnames
  • Public keys
  • Public configuration
  • Example/template files

Never commit:

  • Passwords
  • Private keys
  • API tokens
  • Auth credentials

2. Use .gitignore

# Secrets
variables.nix
secrets.nix
*.secret.nix
*.age
*.key
*-password
*-token

# Directories
secrets/
.secrets/

3. Verify Before Pushing

# Check what will be committed
git status
git diff --cached

# Search for potential secrets
git diff --cached | grep -i "password\|secret\|key"

4. Separate Public and Private Repos

  • Public: Example configs, documentation
  • Private: Your actual variables.nix and secrets

5. Use Secret Scanners

Install pre-commit hooks:

# Install gitleaks
nix-shell -p gitleaks

# Scan for secrets
gitleaks detect

Recovery

Accidentally Committed a Secret

Immediately:

  1. Rotate the secret (change password, revoke token)
  2. Remove from Git history:
# Use BFG Repo-Cleaner
nix-shell -p bfg-repo-cleaner

bfg --replace-text passwords.txt .git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
  1. Force push (if remote)
git push --force
  1. Notify anyone who cloned the repo

Example: Complete Secret Setup

Directory Structure

NullOS/
├── .gitignore           # Ignore secrets
├── variables.nix        # Gitignored
├── secrets/            # Gitignored
│   ├── restic-password
│   ├── vpn-config
│   └── api-keys.env
└── secrets.age/        # agenix encrypted
    ├── secrets.nix
    └── *.age

.gitignore

variables.nix
secrets/
secrets.nix
*.age
!secrets.age/secrets.nix
*.secret.*

Configuration

# modules/services/backup.nix
{
  services.restic.backups.nsdata = {
    repository = vars.resticRepository;  # From variables.nix
    passwordFile = "/etc/nixos/secrets/restic-password";
    
    paths = [ "/home/${vars.username}" ];
    
    exclude = [
      "/home/${vars.username}/.cache"
      "/home/${vars.username}/.secrets"
    ];
  };
}

Next Steps

Clone this wiki locally