Skip to content

Secrets Management

Secrets are managed with agenix, which encrypts secrets at rest using age and decrypts them at NixOS activation time. Secrets never enter the Nix store.

Source: server/secrets/secrets.nix, server/modules/restic.nix (identity paths)

How agenix Works

  1. Secrets are encrypted with age using one or more public keys (SSH Ed25519 keys)
  2. Encrypted .age files are committed to the Git repository
  3. At NixOS activation, agenix decrypts them using the corresponding private key on the host
  4. Decrypted secrets are placed in /run/agenix/ (tmpfs) with restricted file permissions
┌──────────────────────────────────────────────┐
│  Developer workstation                       │
│  age -e -R secrets.nix secret.txt            │
│     └── secret.age (committed to git)        │
└──────────────┬───────────────────────────────┘
               │ git push
┌──────────────▼───────────────────────────────┐
│  Server (NixOS activation)                   │
│  agenix reads /persist/etc/ssh/ssh_host_*    │
│     └── decrypts to /run/agenix/secret       │
│          owner: service user                 │
│          mode: 440                           │
└──────────────────────────────────────────────┘

Identity Paths

Because the server uses impermanence (root is wiped on every boot), SSH host keys live on the persistent volume:

nix
age.identityPaths = [ "/persist/etc/ssh/ssh_host_ed25519_key" ];

This is configured in server/modules/restic.nix and applies globally. Without this, agenix would look for keys at /etc/ssh/ which is wiped on boot.

Critical Path

If /persist/etc/ssh/ssh_host_ed25519_key is lost, all secrets become undecryptable. This key is backed up by Restic (the /persist/etc/ssh directory is in the backup paths). Keep an offline copy of this key.

Public Keys

Two public keys can encrypt secrets:

nix
let
  PublicKeys = [
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBjCf1PpvoMshFkoyjFOYUJ6/pLexwEFqr29COJawkoB"
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILhLtSav5LaQK7/F79Kg+xAZUty68E4sf2gPNUgfu7IP"
  ];

Both keys can decrypt any secret. This allows re-keying from either the server itself or a separate admin machine.

Secret Inventory

All secrets and their consuming services:

Secret FileServiceOwnerPurpose
restic_password.ageResticrootBackup repository encryption passphrase
cloudflare_dns_token.ageTraefikContainerCloudflare API token for DNS-01 ACME challenges
cloudflare_tunnel_token.ageCloudflaredcloudflaredTunnel authentication token
authelia_jwtSecretFile.ageAutheliaContainerJWT signing secret
authelia_sessionSecretFile.ageAutheliaContainerSession encryption secret
authelia_storageEncryptionKeyFile.ageAutheliaContainerSQLite storage encryption key
authelia_oidcIssuerPrivateKeyFile.ageAutheliaContainerOIDC token signing private key
authelia_oidcHmacSecretFile.ageAutheliaContainerOIDC HMAC secret
authelia_users_database.ageAutheliaContainerUser credentials YAML file
nextcloud_client_secret.ageOpenCloudContainerOIDC client secret for Nextcloud/OpenCloud
nextcloud_adminpassFile.ageOpenCloudContainerAdmin password

Secret Permissions

Each secret specifies ownership and permissions:

nix
age.secrets.cloudflare_tunnel_token = {
  mode = "440";
  owner = "cloudflared";
  group = "cloudflared";
  file = ../secrets/cloudflare_tunnel_token.age;
};
FieldValueEffect
mode"440"Owner and group can read; no one else can access
ownerService userOnly the service user can read the secret
groupService groupGroup members can also read (useful for containers)
fileRelative pathPath to the encrypted .age file in the repo

Secrets for Containers

Secrets consumed by NixOS containers are decrypted on the host and bind-mounted into the container's filesystem. The container never has direct access to the age private key.

Adding a New Secret

  1. Create the secret file:
bash
echo -n "my-secret-value" > /tmp/new_secret.txt
  1. Add the secret to secrets.nix:
nix
"new_secret.age".publicKeys = PublicKeys;
  1. Encrypt with agenix:
bash
cd server/secrets
agenix -e new_secret.age < /tmp/new_secret.txt
rm /tmp/new_secret.txt
  1. Reference in your module:
nix
age.secrets.new_secret = {
  mode = "440";
  owner = "myservice";
  group = "myservice";
  file = ../secrets/new_secret.age;
};
  1. Use in service config:
nix
serviceConfig.EnvironmentFile = config.age.secrets.new_secret.path;
# or
ExecStart = "... --secret-file ${config.age.secrets.new_secret.path}";

Re-keying Secrets

If you need to change which public keys can decrypt secrets (e.g., after rotating the server's SSH host key):

bash
cd server/secrets
agenix -r  # re-encrypts all secrets with the keys listed in secrets.nix

This requires access to at least one of the current private keys to decrypt the secrets before re-encrypting them with the new key set.