This repository uses ragenix (a Rust implementation of agenix) for managing encrypted secrets.
- Secrets are encrypted with age using SSH public keys
- Each machine has its own SSH host key for decryption
- Secrets are decrypted at boot time and placed in
/run/agenix/ - Only machines with the corresponding private key can decrypt their secrets
secrets/
├── machines/ # Per-machine secrets (NixOS hosts, encrypted)
│ └── <hostname>/
│ ├── sshd/ # SSH host keys
│ ├── init-sshd/ # Init SSH keys (for remote unlock)
│ ├── wg-vpn/ # WireGuard keys
│ └── nebula/ # Nebula certificates
├── devices/ # External devices (phones, etc.)
│ └── <device-name>/
│ ├── public_key # Device's Nebula public key (not encrypted)
│ └── host.crt.age # Signed certificate (encrypted)
├── services/ # Service credentials
│ ├── nebula/ # Nebula CA (ca.key.age, ca.crt.age)
│ ├── vaultwarden/
│ └── ...
├── system/ # System-level secrets
└── users/ # User passwords and keys
└── <username>/
└── hashed-password.age
| File | Purpose |
|---|---|
secrets.nix |
Defines which keys can decrypt which secrets |
nixos-secrets.nix |
Declares secrets for NixOS (age.secrets) |
shells/secrets.nix |
Development shell with ragenix and provision-secrets |
packages/provision-secrets.nix |
Tool for generating machine secrets |
nix develop .#secretsThis provides:
ragenix- Encrypt/decrypt individual secretsprovision-secrets- Generate all secrets for a new machinenebula-cert- Nebula certificate managementwg- WireGuard key generation
# Interactive mode - prompts for each secret type
provision-secrets <machine>
# Generate all secrets at once
provision-secrets <machine> --all --nebula-ip 10.101.0.X/24
# Generate specific secret types
provision-secrets <machine> --ssh
provision-secrets <machine> --wireguard
provision-secrets <machine> --nebula --nebula-ip 10.101.0.X/24
# Overwrite existing secrets
provision-secrets <machine> --force --sshragenix -e secrets/users/lriutzel/hashed-password.ageAfter modifying secrets.nix (adding/removing machine keys):
ragenix --rekeyGenerated by provision-secrets --ssh:
sshd/private_key.age- Encrypted private keysshd/public_key- Public key (not encrypted)
For remote LUKS unlock during boot:
init-sshd/private_key.age- Encrypted private keyinit-sshd/public_key- Public key
Generated by provision-secrets --wireguard:
wg-vpn/private.age- Encrypted private keywg-vpn/public.age- Encrypted public key
After generation, add the public key to modules/nixos/gumdrop/vpn.nix peers.
Generated by provision-secrets --nebula:
nebula/host.crt.age- Encrypted host certificatenebula/host.key.age- Encrypted host private key
The CA certificate is shared from secrets/services/nebula/ca.crt.age.
The Nebula CA is stored centrally and used to sign all host certificates:
# Initialize CA (one time only)
nebula-cert ca -name "Gumdrop Nebula CA"
# Encrypt and store CA
mkdir -p secrets/services/nebula
cat ca.key | ragenix --editor - -e secrets/services/nebula/ca.key.age
cat ca.crt | ragenix --editor - -e secrets/services/nebula/ca.crt.age
# Clean up unencrypted files
rm ca.key ca.crtThe provision-secrets --nebula command automatically decrypts the CA to sign new host certificates.
The recommended workflow:
# 1. Enter secrets shell
nix develop .#secrets
# 2. Generate all secrets
provision-secrets <machine> --all --nebula-ip 10.101.0.X/24
# 3. Add WireGuard public key to vpn.nix
# (provision-secrets outputs the key)
# 4. Configure machine
# In machines/<machine>/configuration.nix:
gumdrop = {
vpn.client.enable = true;
vpn.client.ip = "10.100.0.X/24";
nebula.client.enable = true;
nebula.client.ip = "10.101.0.X/24";
};For devices that aren't NixOS machines (phones, tablets, external laptops), the workflow is different because the device generates its own private key.
- On device: Generate keypair in Nebula app → export public key
- On computer: Sign public key with CA using
provision-secrets - On device: Import certificate + CA cert
# Enter secrets shell
nix develop .#secrets
# Sign the device's public key
provision-secrets lucas-phone --sign-device --pubkey ~/Downloads/phone.pub --nebula-ip 10.101.0.20/24
# Output files will be in secrets/devices/lucas-phone/:
# - public_key (copy of the device's public key, not encrypted)
# - host.crt.age (signed certificate, encrypted)Decrypt the files for transfer:
# Decrypt host certificate
age -d -i ~/.ssh/id_ed25519 secrets/devices/lucas-phone/host.crt.age > host.crt
# Decrypt CA certificate
age -d -i ~/.ssh/id_ed25519 secrets/services/nebula/ca.crt.age > ca.crtTransfer host.crt and ca.crt to the device via:
- QR code (Nebula app can scan)
- AirDrop / file share
- USB / local network
Then delete the decrypted files:
rm host.crt ca.crt# 1. On phone: Open Nebula app → Generate Keys → Export Public Key
# (save to phone.pub and transfer to computer)
# 2. On computer - sign the key:
nix develop .#secrets
provision-secrets lucas-phone --sign-device --pubkey ~/phone.pub --nebula-ip 10.101.0.20/24
# 3. Decrypt for transfer:
age -d -i ~/.ssh/id_ed25519 secrets/devices/lucas-phone/host.crt.age > host.crt
age -d -i ~/.ssh/id_ed25519 secrets/services/nebula/ca.crt.age > ca.crt
# 4. Transfer host.crt and ca.crt to phone (AirDrop, etc.)
# 5. On phone: Import CA cert and host cert in Nebula app
# Configure lighthouse: vpn.lucasr.com:4242
# 6. Clean up decrypted files:
rm host.crt ca.crt| Aspect | Machines (secrets/machines/) |
Devices (secrets/devices/) |
|---|---|---|
| Private key | Generated here, encrypted with age | Generated on device, stays on device |
| Certificate | Encrypted (.age) | Encrypted (.age) |
| Managed by | NixOS/agenix | Device's Nebula app |
| Decryptable by | Users + machine's SSH key | Users only |
| Use case | NixOS hosts | Phones, tablets, external clients |
{ config, ... }:
{
age.secrets = {
lriutzel-hashed-password = {
file = ./secrets/users/lriutzel/hashed-password.age;
owner = "root";
group = "root";
mode = "400";
};
vaultwarden-env = {
file = ./secrets/services/vaultwarden/env.age;
owner = "vaultwarden";
};
};
}{ config, ... }:
{
users.users.lriutzel = {
hashedPasswordFile = config.age.secrets.lriutzel-hashed-password.path;
};
services.vaultwarden = {
environmentFile = config.age.secrets.vaultwarden-env.path;
};
}- Verify the machine's SSH host key is in
secrets.nix - Run
ragenix --rekeyafter adding the key - Ensure the machine has its private key at
/etc/ssh/ssh_host_ed25519_key
- Check the
owner,group, andmodein the secret declaration - Verify the service user exists before the secret is created
- Initialize the CA first (see Nebula CA Management above)
- Verify
secrets/services/nebula/ca.key.ageexists
- Create the secret file:
ragenix -e secrets/services/<name>/secret.age - Declare it in
nixos-secrets.nix - Reference it in your service configuration