Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/soriphoono/homelab/llms.txt

Use this file to discover all available pages before exploring further.

This guide covers managing secrets in your homelab using SOPS (Secrets OPerationS) with age encryption.

Overview

Secrets are managed using:
  • SOPS: For encrypting and managing secret files
  • age: Modern encryption tool using SSH keys
  • agenix: NixOS integration for SOPS
  • agenix-shell: Development shell integration

Secret Architecture

User-Based Access

Secrets are organized by user and team access:
users = {
  username = "ssh-ed25519 AAAA...";
};

# Per-user secrets
userSecrets = {
  api_key = [ "username" ];
};

# Team secrets
teams = {
  cloud = {
    users = ["username" "admin"];
    secrets = ["proxmox_api_secret" "cloudflare_token"];
  };
};

Setting Up Secrets

1

Generate SSH Key (If Needed)

Create an ED25519 SSH key for encryption:
ssh-keygen -t ed25519 -C "your@email.com"
This key will be used for encrypting/decrypting secrets.
2

Configure secrets.nix

Create or edit secrets.nix in your project root:
secrets.nix
let
  secretsFunction = {lib, ...}: let
    users = {
      soriphoono = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEgxxFcqHVwYhY0TjbsqByOYpmWXqzlVyGzpKjqS8mO7";
      admin = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...";
    };

    # Per-user secrets: simple mapping of secret name -> list of users
    userSecrets = {
      github_token = [ "soriphoono" ];
      aws_credentials = [ "admin" ];
    };

    # Team secrets: for shared access across multiple users
    teams = {
      cloud = {
        users = ["soriphoono" "admin"];
        secrets = ["proxmox_api_secret" "cloudflare_token"];
      };
      
      database = {
        users = ["admin"];
        secrets = ["postgres_password" "redis_password"];
      };
    };

    # Helper: create a secret entry with public keys
    mkSecret = name: userList: {
      name = "secrets/${name}.age";
      value = {
        publicKeys = builtins.map (user: users.${user}) userList;
      };
    };

    # Collect secrets from teams
    teamSecretsList = builtins.concatLists (
      builtins.map (team: 
        builtins.map (secret: mkSecret secret team.users) team.secrets
      ) (builtins.attrValues teams)
    );

    # Collect per-user secrets
    userSecretsList = builtins.attrValues (
      lib.mapAttrs' (secret: userList: mkSecret secret userList) userSecrets
    );

    # Shell secrets for agenix-shell
    envUser = builtins.getEnv "USER";
    currentUser = if envUser == "" then "soriphoono" else envUser;
    userTeams = builtins.filter (team: builtins.elem currentUser team.users) (builtins.attrValues teams);
    userOwnSecrets = builtins.attrNames (lib.filterAttrs (_: userList: builtins.elem currentUser userList) userSecrets);

    agenix-shell-secrets = lib.listToAttrs (builtins.concatLists [
      # Secrets from teams the user belongs to
      (builtins.concatLists (builtins.map (
          team:
            builtins.map (secret: {
              name = lib.toUpper secret;
              value.file = ./secrets/${secret}.age;
            })
            team.secrets
        )
        userTeams))
      # Per-user secrets the user has access to
      (builtins.map (secret: {
          name = lib.toUpper secret;
          value.file = ./secrets/${secret}.age;
        })
        userOwnSecrets)
    ]);
  in
    {
      inherit agenix-shell-secrets;
    }
    // builtins.listToAttrs (teamSecretsList ++ userSecretsList);

  # Minimal lib for the default call
  libMinimal = {
    inherit (builtins) listToAttrs concatLists;
    mapAttrs' = f: set:
      builtins.listToAttrs (builtins.map (n: f n set.${n}) (builtins.attrNames set));
    filterAttrs = f: set:
      builtins.listToAttrs (builtins.map (n: {
        name = n;
        value = set.${n};
      }) (builtins.filter (n: f n set.${n}) (builtins.attrNames set)));
    toUpper = s: s;
  };
in
  (secretsFunction {lib = libMinimal;})
  // {
    __functor = _: secretsFunction;
  }
3

Create secrets Directory

mkdir -p secrets
4

Add User to secrets.nix

Add your SSH public key to the users attribute:
users = {
  yourname = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...";
};
5

Create a Secret File

Use agenix to create and encrypt a secret:
# Enter dev shell with agenix
nix develop

# Create/edit secret
agenix -e secrets/proxmox_api_secret.age
This opens your $EDITOR to input the secret value.
6

Enable Secrets Module in System

In your system configuration:
systems/yoursystem/default.nix
core.secrets = {
  enable = true;
  defaultSopsFile = ./secrets.yaml;  # If using YAML
};
7

Commit Encrypted Secrets

git add secrets/*.age secrets.nix
git commit -m "feat: add secret management configuration"
Only commit .age encrypted files, never plain text secrets!

Using Secrets

In NixOS Configuration

Reference secrets in your system configuration:
sops.secrets."proxmox_api_secret" = {
  sopsFile = ./secrets/proxmox_api_secret.age;
  owner = "root";
  group = "root";
  mode = "0400";
};

# Use in service configuration
services.myservice = {
  enable = true;
  apiKeyFile = config.sops.secrets."proxmox_api_secret".path;
};

In Development Shell

Secrets are automatically loaded as environment variables in the dev shell:
nix develop

# Access secrets as environment variables
echo $PROXMOX_API_SECRET

In Home Manager

For user-level secrets:
core.secrets.enable = true;

# Creates ~/.config/sops/age/keys.txt

Secret Management Workflows

  1. Update secrets.nix with user/team access
  2. Create the secret file:
    agenix -e secrets/newsecret.age
    
  3. Commit the encrypted file:
    git add secrets/newsecret.age
    git commit -m "feat: add new secret for service X"
    
  1. Edit the existing secret:
    agenix -e secrets/existing.age
    
  2. Update the value and save
  3. Commit the change:
    git add secrets/existing.age
    git commit -m "chore: rotate existing secret"
    
  4. Deploy to systems:
    nixos-rebuild switch --flake .#system
    
  1. Get user’s SSH public key
  2. Add to secrets.nix:
    users.newuser = "ssh-ed25519 AAAAC3...";
    
  3. Add user to team or grant individual access:
    teams.cloud.users = ["existinguser" "newuser"];
    
  4. Re-encrypt all secrets:
    agenix -r
    
  5. Commit changes:
    git add secrets.nix secrets/*.age
    git commit -m "chore: grant secret access to newuser"
    
  1. Remove user from secrets.nix
  2. Re-encrypt secrets:
    agenix -r
    
  3. Commit and deploy immediately

Secret Organization

Directory Structure

.
├── secrets.nix           # Secret access configuration
├── secrets/
│   ├── proxmox_api_secret.age
│   ├── cloudflare_token.age
│   ├── github_token.age
│   └── postgres_password.age
└── systems/
    └── myserver/
        └── secrets.yaml  # System-specific secrets (optional)

Naming Conventions

  • Use lowercase with underscores: api_key.age
  • Be descriptive: cloudflare_api_token.age not cf.age
  • Group by service: postgres_password.age, postgres_user.age

Templates Integration

The empty template includes secrets configuration:
nix flake init -t .#empty
This creates:
  • secrets.nix with base configuration
  • shell.nix with agenix integration
  • Pre-configured flake inputs for agenix

Security Best Practices

  • Never commit unencrypted secrets
  • Always use .gitignore for plain text secret files
  • Rotate secrets after removing user access
  • Use team-based access for shared secrets
  • Keep SSH private keys secure
  • Use different secrets for dev/staging/prod
  • Document secret purpose in secrets.nix comments
  • Regular secret rotation improves security
  • Use read-only permissions (0400) where possible

Troubleshooting

Cannot Decrypt Secret

# Verify your SSH key is loaded
ssh-add -l

# Check secrets.nix contains your public key
grep "your-key" secrets.nix

Secret Not Available in Shell

# Ensure agenix-shell is configured in flake.nix
# Exit and re-enter dev shell
exit
nix develop

Re-encrypting All Secrets

# After updating secrets.nix
agenix -r

Next Steps

Build docs developers (and LLMs) love