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
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. Configure secrets.nix
Create or edit secrets.nix in your project root: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;
}
Add User to secrets.nix
Add your SSH public key to the users attribute:users = {
yourname = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...";
};
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. Enable Secrets Module in System
In your system configuration:systems/yoursystem/default.nix
core.secrets = {
enable = true;
defaultSopsFile = ./secrets.yaml; # If using YAML
};
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
- Update
secrets.nix with user/team access
- Create the secret file:
agenix -e secrets/newsecret.age
- Commit the encrypted file:
git add secrets/newsecret.age
git commit -m "feat: add new secret for service X"
- Edit the existing secret:
agenix -e secrets/existing.age
- Update the value and save
- Commit the change:
git add secrets/existing.age
git commit -m "chore: rotate existing secret"
- Deploy to systems:
nixos-rebuild switch --flake .#system
Granting Access to a User
- Get user’s SSH public key
- Add to
secrets.nix:
users.newuser = "ssh-ed25519 AAAAC3...";
- Add user to team or grant individual access:
teams.cloud.users = ["existinguser" "newuser"];
- Re-encrypt all secrets:
- Commit changes:
git add secrets.nix secrets/*.age
git commit -m "chore: grant secret access to newuser"
- Remove user from
secrets.nix
- Re-encrypt secrets:
- 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