Skip to content

Secrets Management

All secrets are managed using a hybrid approach combining 1Password as the primary source of truth, and Agenix for the initial deployment bootstrap. This ensures no sensitive secrets are exposed in the git repository while maintaining a fully automated deployment.

  1. Agenix for the Bootstrap Token: We use Agenix to securely encrypt a single file in the repository: 1password-token.age. This file contains the 1Password Service Account Token. NixOS decrypts this file at boot using the host’s private SSH key (/etc/ssh/ssh_host_ed25519_key).
  2. 1Password for Everything Else: Once the 1password-token is available on the machine, NixOS systemd services (like cloudflared) use the 1Password CLI (op) to fetch the rest of the required credentials dynamically at runtime.

The homelab uses two separate SSH keys stored in 1Password under the Homelab vault:

Key1Password ItemPurposeComment
Host IdentityHomelab SSHServer host key (/etc/ssh/ssh_host_ed25519_key). Used by agenix to decrypt 1password-token.age at boot.root@nixos
User AuthHomelab SSH - javierscPersonal SSH key for authenticating to the server. Deployed by ssh-key-setup service.

How it works:

  1. The ssh-key-setup oneshot service runs before sshd.service on every boot.
  2. It fetches the public and private key from the Homelab SSH - javiersc 1Password item using the Service Account token.
  3. It writes them to ~/.ssh/ (id_ed25519 and authorized_keys) with correct permissions and ownership.
  4. SSH is configured with PasswordAuthentication = false — key-only authentication.
  5. The SSH proxy (homelab.proxies.ssh) exposes SSH via Cloudflare tunnel.

Client setup (any OS):

  1. Export the private key from the Homelab SSH - javiersc 1Password item to ~/.ssh/id_ed25519.
  2. Connect: ssh javiersc@nixos.local or ssh ssh-home.javiersc.com (via Cloudflare).

Works on Windows, macOS, and Linux.

To maintain security, the infrastructure only has access to the specific secrets it needs:

  1. Dedicated Vault: A specific vault (e.g., Homelab) contains only the items required for this infrastructure (Cloudflare tokens, etc.).
  2. Service Account: A 1Password Service Account is created and granted “Read-Only” access exclusively to that dedicated vault.
  3. Token-based Auth: The server uses the Service Account Token (injected via Agenix) to authenticate. If the machine is ever compromised, your personal vaults and other sensitive data remain completely isolated and inaccessible.

To inject secrets securely, we use a unified set of NixOS helpers defined in the infrastructure repository. This follows security best practices:

  1. Unified Helper Flow:
    • homelab.mkSecretService: Creates a dedicated oneshot service (e.g., litellm-secrets) to fetch credentials. It runs as root, fetches secrets with automatic retries (inner retry_op 12x + outer systemd 3x restart), writes them to a private RAM-based directory (/run/<service>/env), and sets restrictive permissions (0700).
    • Companion Config Service: When a service needs structured config files (JSON/YAML) with embedded secrets, a second oneshot service (e.g., litellm-config) sources /run/<service>/env and renders the config files using envsubst. Secrets are interpolated via quoted heredocs (<<'EOF') to prevent shell injection.
    • homelab.serviceConfig: Used on the main service only (not the secrets/config oneshots) to set up RuntimeDirectory, 0700 permissions, and load the 1Password token via systemd LoadCredential.
  2. Resilience & Security:
    • Automatic Retries: All 1Password fetching includes retry_op with flock-based concurrency control (12 attempts, exponential backoff) plus systemd-level Restart=on-failure (3 restarts). Services depending on secrets use after / requires chains so they only start after secrets are available.
    • Safe Heredocs: Config file templates use quoted heredocs (<<'EOF') piped through envsubst. This prevents shell command injection from secret values containing $(), backticks, or other metacharacters.
    • RAM-based storage: Secrets never touch the disk; they live in tmpfs (/run) and are automatically wiped on service stop or reboot.
    • Leak Prevention: Secrets are passed via EnvironmentFile or file-based reading, preventing them from appearing in systemctl status or /proc. No pkgs.writeText or Nix store artifacts for secrets.
  3. DynamicUser Support: For services that use systemd DynamicUser (where the user ID changes on every start), we use an inline ExecStartPre hook via homelab.fetchSecretFile to fetch secrets directly within the service context.

You can still interact with secrets manually using the 1Password CLI:

Terminal window
# Authenticate (if not using a Service Account token locally)
op signin
# Read a specific secret
op read "op://Homelab/Cloudflare/api-token"