cryodev/docs/services/sops.md
steffen dbf98e2f22 add .gitignore, fix headscale CLI to use numeric user IDs
- Add .gitignore for nix build result symlinks
- Fix all headscale CLI commands: --user now requires numeric ID,
  not username (changed in newer headscale versions)
- Add 'headscale users list' step to docs where preauth keys are created
2026-03-14 12:28:47 +01:00

3.3 KiB

SOPS Secret Management

Atomic secret provisioning for NixOS using sops-nix.

Overview

Secrets are encrypted with age using SSH host keys, ensuring:

  • No plaintext secrets in the repository
  • Secrets are decrypted at activation time
  • Each host can only decrypt its own secrets

Setup

1. Get Host's Age Public Key

After a host is installed, extract its age key from the SSH host key:

nix-shell -p ssh-to-age --run 'ssh-keyscan -t ed25519 <HOST_IP> | ssh-to-age'

Or locally on the host:

nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'

2. Configure .sops.yaml

Add the host key to .sops.yaml:

keys:
  - &admin_key age1e8p35795htf7twrejyugpzw0qja2v33awcw76y4gp6acnxnkzq0s935t4t
  - &main_key age1...  # cryodev-main
  - &pi_key age1...    # cryodev-pi

creation_rules:
  - path_regex: hosts/cryodev-main/secrets.yaml$
    key_groups:
      - age:
        - *admin_key
        - *main_key
        
  - path_regex: hosts/cryodev-pi/secrets.yaml$
    key_groups:
      - age:
        - *admin_key
        - *pi_key

3. Create Secrets File

sops hosts/<hostname>/secrets.yaml

This opens your editor. Add secrets in YAML format:

tailscale:
  auth-key: "tskey-..."
  
some-service:
  password: "secret123"

Usage in Modules

Declaring Secrets

{ config, ... }:
{
  sops.secrets.my-secret = {
    # Optional: set owner/group
    owner = "myservice";
    group = "myservice";
  };
}

Using Secrets

Reference the secret path in service configuration:

{
  services.myservice = {
    passwordFile = config.sops.secrets.my-secret.path;
  };
}

Using Templates

For secrets that need to be embedded in config files:

{
  sops.secrets."netdata/stream-api-key" = { };
  
  sops.templates."netdata-stream.conf" = {
    content = ''
      [stream]
      enabled = yes
      api key = ${config.sops.placeholder."netdata/stream-api-key"}
    '';
    owner = "netdata";
  };
  
  services.netdata.configDir."stream.conf" = 
    config.sops.templates."netdata-stream.conf".path;
}

Common Secrets

cryodev-main

mailserver:
  accounts:
    forgejo: "$2y$05$..."  # bcrypt hash
    admin: "$2y$05$..."

forgejo-runner:
  token: "..."

headplane:
  cookie_secret: "..."       # openssl rand -hex 16
  agent_pre_authkey: "..."   # headscale preauthkey

tailscale:
  auth-key: "tskey-..."

cryodev-pi

tailscale:
  auth-key: "tskey-..."

netdata:
  stream:
    child-uuid: "..."  # uuidgen

Generating Secret Values

Secret Command
Mailserver password nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
Random hex token nix-shell -p openssl --run 'openssl rand -hex 16'
UUID uuidgen
Tailscale preauth sudo headscale preauthkeys create --expiration 99y --reusable --user <ID>

Updating Keys

After modifying .sops.yaml, update existing secrets files:

sops --config .sops.yaml updatekeys hosts/<hostname>/secrets.yaml

Troubleshooting

"No matching keys found"

Ensure the host's age key is in .sops.yaml and you've run updatekeys.

Secret not decrypting on host

Check that /etc/ssh/ssh_host_ed25519_key exists and matches the public key in .sops.yaml.