Skip to content

Latest commit

 

History

History
298 lines (219 loc) · 8.21 KB

File metadata and controls

298 lines (219 loc) · 8.21 KB

Secrets Management

This repository uses ragenix (a Rust implementation of agenix) for managing encrypted secrets.

How It Works

  1. Secrets are encrypted with age using SSH public keys
  2. Each machine has its own SSH host key for decryption
  3. Secrets are decrypted at boot time and placed in /run/agenix/
  4. Only machines with the corresponding private key can decrypt their secrets

Directory Structure

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

Key Files

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

Working with Secrets

Enter the secrets shell

nix develop .#secrets

This provides:

  • ragenix - Encrypt/decrypt individual secrets
  • provision-secrets - Generate all secrets for a new machine
  • nebula-cert - Nebula certificate management
  • wg - WireGuard key generation

Generate secrets for a new machine

# 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 --ssh

View/Edit a secret

ragenix -e secrets/users/lriutzel/hashed-password.age

Re-encrypt secrets after key changes

After modifying secrets.nix (adding/removing machine keys):

ragenix --rekey

Secret Types

SSH Host Keys

Generated by provision-secrets --ssh:

  • sshd/private_key.age - Encrypted private key
  • sshd/public_key - Public key (not encrypted)

Init SSH Host Keys

For remote LUKS unlock during boot:

  • init-sshd/private_key.age - Encrypted private key
  • init-sshd/public_key - Public key

WireGuard VPN

Generated by provision-secrets --wireguard:

  • wg-vpn/private.age - Encrypted private key
  • wg-vpn/public.age - Encrypted public key

After generation, add the public key to modules/nixos/gumdrop/vpn.nix peers.

Nebula Mesh VPN

Generated by provision-secrets --nebula:

  • nebula/host.crt.age - Encrypted host certificate
  • nebula/host.key.age - Encrypted host private key

The CA certificate is shared from secrets/services/nebula/ca.crt.age.

Nebula CA Management

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.crt

The provision-secrets --nebula command automatically decrypts the CA to sign new host certificates.

Adding a New Machine's Secrets

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";
};

Adding Mobile Devices (Phones, External Clients)

For devices that aren't NixOS machines (phones, tablets, external laptops), the workflow is different because the device generates its own private key.

Workflow

  1. On device: Generate keypair in Nebula app → export public key
  2. On computer: Sign public key with CA using provision-secrets
  3. On device: Import certificate + CA cert

Sign a device's public key

# 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)

Transfer to device

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.crt

Transfer 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

Example: Adding a phone

# 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

Device vs Machine secrets

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

Secret Declaration (nixos-secrets.nix)

{ 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";
    };
  };
}

Using Secrets in Configuration

{ config, ... }:
{
  users.users.lriutzel = {
    hashedPasswordFile = config.age.secrets.lriutzel-hashed-password.path;
  };

  services.vaultwarden = {
    environmentFile = config.age.secrets.vaultwarden-env.path;
  };
}

Troubleshooting

Secret not decrypting

  • Verify the machine's SSH host key is in secrets.nix
  • Run ragenix --rekey after adding the key
  • Ensure the machine has its private key at /etc/ssh/ssh_host_ed25519_key

Permission denied

  • Check the owner, group, and mode in the secret declaration
  • Verify the service user exists before the secret is created

Nebula CA not found

  • Initialize the CA first (see Nebula CA Management above)
  • Verify secrets/services/nebula/ca.key.age exists

Adding secrets for a new service

  1. Create the secret file: ragenix -e secrets/services/<name>/secret.age
  2. Declare it in nixos-secrets.nix
  3. Reference it in your service configuration