Skip to content

Secrets Setup

This tutorial teaches you how secrets management works in this infrastructure. You’ll understand the secrets architecture and migration state, derive your age encryption key from your SSH key, and set up secrets that deploy automatically with your configuration.

By the end of this tutorial, you will understand:

  • The clan vars target architecture and current migration state
  • How age encryption provides the foundation for secrets management
  • The Bitwarden bootstrap workflow for key derivation
  • How sops-nix integrates secrets into your home-manager configuration
  • The migration path from legacy sops-nix to clan vars

Before starting, you should have:

  • Completed the Bootstrap to Activation Tutorial
  • Your machine activated and working
  • SSH key stored in Bitwarden (or access to create one)
  • Bitwarden CLI (bw) available in your environment

30-45 minutes for initial setup. Adding new secrets takes 5-10 minutes once you understand the workflow.

Before handling any secrets, let’s understand the target architecture and current migration state.

The long-term architecture uses clan vars for all secrets management across all platforms. Clan vars uses sops encryption internally and provides:

Unified management: All secrets (system and user) managed through the clan inventory.

Automatic generation: System secrets like SSH host keys and zerotier identities are generated automatically by clan during deployment.

Declarative configuration: Secrets are defined declaratively and deployed to appropriate locations (/run/secrets/ for system, ~/.config/sops-nix/secrets/ for user).

Cross-platform support: Works on both NixOS and darwin (nix-darwin).

Current state: clan vars primary, sops-nix for user-level secrets

Section titled “Current state: clan vars primary, sops-nix for user-level secrets”

This infrastructure uses two complementary approaches:

Clan vars (primary system):

  • System secrets on NixOS machines (SSH host keys, zerotier identities, service credentials)
  • Managed automatically by clan
  • Deployed to /run/secrets/ at system activation
  • Available on both darwin and NixOS platforms

sops-nix (user-level secrets):

  • User secrets managed manually
  • Created and encrypted by you
  • Deployed to ~/.config/sops-nix/secrets/ at home-manager activation
  • Used for personal credentials and user-specific secrets

Using clan vars for system secrets and sops-nix for user secrets provides:

Appropriate automation: Clan automatically generates and manages system-level secrets (SSH host keys, service credentials) while user secrets remain under explicit user control.

Clear separation of concerns: System infrastructure secrets handled by clan orchestration, personal credentials handled by individual users.

Consistent tooling: Same sops encryption foundation for both approaches, with clan vars adding generation and deployment automation for system secrets.

For the complete architecture reference, see Clan Integration: Secrets Management.

AspectDarwin (macOS)NixOS
Clan varsPlanned (not yet implemented)Active for system secrets
sops-nixActive for user secretsActive for user secrets
System secretsManualGenerated by clan vars
User secretssops-nixsops-nix

Clan vars is currently NixOS-only. Darwin support is planned but not yet implemented. User secrets on both platforms use sops-nix.

Before setting up secrets, understand the encryption foundation.

Age is a modern encryption tool designed for simplicity and security. Unlike GPG with its complex web of trust, age uses a single key pair: a private key for decryption and a public key for encryption.

In this infrastructure:

  • Your age private key lives at ~/.config/sops/age/keys.txt and decrypts your secrets
  • Your age public key is listed in .sops.yaml so sops knows which keys can decrypt which files

Rather than managing separate age keys, we derive age keys from your existing SSH keys. This provides:

Fewer keys to manage: Your SSH key already exists and is backed up. Deriving the age key means one fewer secret to track.

Consistent identity: Your SSH key represents your identity for git commits, server access, and now secret decryption.

Bitwarden integration: SSH keys stored in Bitwarden’s SSH Agent feature can be exported for age derivation, keeping your key management centralized.

Step 2: Retrieve your SSH key from Bitwarden

Section titled “Step 2: Retrieve your SSH key from Bitwarden”

The bootstrap workflow retrieves your SSH key from Bitwarden, derives an age key from it, and stores the age key for sops-nix to use.

Terminal window
# Check if already logged in
bw status
# If locked, unlock
export BW_SESSION=$(bw unlock --raw)
# If not logged in, login first
bw login
export BW_SESSION=$(bw unlock --raw)
Terminal window
# List items containing "ssh" in the name
bw list items --search "ssh" | jq '.[].name'
# Get the specific item ID
bw list items --search "your-ssh-key-name" | jq '.[0].id'
Terminal window
# Get the SSH private key attachment or notes field
# The exact command depends on how you stored the key
# If stored as an attachment:
bw get attachment "id_ed25519" --itemid "your-item-id" --output /tmp/ssh_key
# If stored in secure notes, adjust accordingly

Security note: We write to /tmp/ because this is temporary. We’ll derive the age key and then securely delete the SSH key from disk.

Now convert your SSH key to an age key using ssh-to-age.

Terminal window
# Derive age public key from SSH public key
ssh-to-age -i /tmp/ssh_key.pub
# Output: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Save this output; you’ll need it for .sops.yaml.

Terminal window
# Create the sops directory
mkdir -p ~/.config/sops/age
# Derive age private key from SSH private key
ssh-to-age -private-key -i /tmp/ssh_key > ~/.config/sops/age/keys.txt
# Set restrictive permissions
chmod 600 ~/.config/sops/age/keys.txt
Terminal window
# Check the key file exists and has content
head -1 ~/.config/sops/age/keys.txt
# Should show: AGE-SECRET-KEY-1...
# Verify permissions
ls -la ~/.config/sops/age/keys.txt
# Should show: -rw------- (600)
Terminal window
# Securely delete the temporary SSH key
rm -P /tmp/ssh_key /tmp/ssh_key.pub 2>/dev/null || rm /tmp/ssh_key /tmp/ssh_key.pub

The .sops.yaml file tells sops which public keys can decrypt which secret files.

Open .sops.yaml in the repository root:

keys:
- &admin age1adminkey...
- &crs58 age1crs58key...
- &raquel age1raquelkey...
creation_rules:
# Admin secrets - only admin can decrypt
- path_regex: secrets/admin/.*\.yaml$
key_groups:
- age:
- *admin
# User secrets - user and admin can decrypt
- path_regex: secrets/users/crs58/.*\.yaml$
key_groups:
- age:
- *admin
- *crs58

The structure defines:

  • keys: Named age public keys (using YAML anchors like &crs58)
  • creation_rules: Which keys can decrypt which file paths

Add your age public key to the keys section:

keys:
- &admin age1adminkey...
- &crs58 age1crs58key...
- &yourusername age1yourpublickey... # Add your key

Then add a creation rule for your secrets:

creation_rules:
# Your user secrets
- path_regex: secrets/users/yourusername/.*\.yaml$
key_groups:
- age:
- *admin
- *yourusername

Why include admin? The admin key provides recovery capability. If you lose access to your age key, the admin can re-encrypt secrets for a new key.

Now create an encrypted secrets file for your user.

Terminal window
mkdir -p secrets/users/yourusername
Terminal window
sops secrets/users/yourusername/secrets.sops.yaml

This opens your $EDITOR with a new encrypted file. Add your secrets in YAML format:

github-token: ghp_yourtoken
some-api-key: sk-yourapikey

Save and close. Sops automatically encrypts the file using the keys specified in .sops.yaml.

Terminal window
# View the raw encrypted file
cat secrets/users/yourusername/secrets.sops.yaml

You’ll see the encrypted content with sops metadata. The values are encrypted; only the structure is visible.

Terminal window
# Decrypt and view
sops -d secrets/users/yourusername/secrets.sops.yaml

If this shows your original values, your age key is working correctly.

Step 6: Integrate secrets with home-manager

Section titled “Step 6: Integrate secrets with home-manager”

Now configure home-manager to deploy your secrets.

In your user module (e.g., modules/home/users/yourusername/default.nix), add sops configuration:

{ config, ... }:
{
sops = {
defaultSopsFile = ../../../../../secrets/users/yourusername/secrets.sops.yaml;
age.keyFile = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
secrets = {
"github-token" = {};
"some-api-key" = {};
};
};
# Use secrets in other configuration
programs.git.extraConfig = {
# Reference the decrypted secret path
credential.helper = "!f() { echo password=$(cat ${config.sops.secrets.github-token.path}); }; f";
};
}

When home-manager activates:

  1. sops-nix reads your encrypted file
  2. Decrypts it using your age key
  3. Writes each secret to ~/.config/sops-nix/secrets/<secret-name>
  4. Sets appropriate permissions (default 0400, owner-read only)

You reference secrets via config.sops.secrets.<name>.path, which resolves to the decrypted file path.

Terminal window
# Rebuild your configuration
just activate
# Check the secrets were deployed
ls -la ~/.config/sops-nix/secrets/
# Verify a secret's content (carefully!)
cat ~/.config/sops-nix/secrets/github-token

You’ve now set up secrets management from scratch. Along the way, you learned:

  • Target architecture uses clan vars for all secrets across all platforms
  • Current migration state still uses legacy sops-nix for user secrets
  • Age encryption provides the cryptographic foundation using simple key pairs
  • SSH key derivation via ssh-to-age reduces key management overhead
  • sops integration encrypts secrets at rest and decrypts them during activation

Now that you understand secrets:

  1. Add more secrets as needed. Use templates for complex formats; see the Secrets Management Guide.

  2. Understand clan vars for the target architecture. Clan vars are generated automatically, and understanding them helps with migration planning.

  3. Continue to platform-specific tutorials:

  4. Review operational procedures in the Secrets Management Guide for rotation, sharing, and troubleshooting.

”No secret key found” during decryption

Section titled “”No secret key found” during decryption”

Your age key isn’t where sops expects it:

Terminal window
# Check the key file exists
ls -la ~/.config/sops/age/keys.txt
# Verify it's a valid age key
head -1 ~/.config/sops/age/keys.txt # Should start with AGE-SECRET-KEY-

Your age key doesn’t match any keys in .sops.yaml:

Terminal window
# Get your age public key
age-keygen -y ~/.config/sops/age/keys.txt
# Compare with what's in .sops.yaml
grep "your-expected-prefix" .sops.yaml

If they don’t match, either update .sops.yaml or regenerate your age key.

Check sops-nix configuration:

Terminal window
# Verify the sops configuration is being evaluated
nix eval .#homeConfigurations.yourusername.config.sops.secrets --json | jq
# Check home-manager logs during activation
home-manager switch --flake .#yourusername --show-trace 2>&1 | grep -i sops

Secrets default to mode 0400 (owner read only). If another user or process needs access:

sops.secrets."shared-secret" = {
mode = "0440"; # Owner and group can read
group = "users";
};

For comprehensive troubleshooting, see the Secrets Management Guide.