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.
What you will learn
Section titled “What you will learn”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
Prerequisites
Section titled “Prerequisites”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
Estimated time
Section titled “Estimated time”30-45 minutes for initial setup. Adding new secrets takes 5-10 minutes once you understand the workflow.
Understanding secrets architecture
Section titled “Understanding secrets architecture”Before handling any secrets, let’s understand the target architecture and current migration state.
Target: clan vars for all secrets
Section titled “Target: clan vars for all secrets”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
Why this dual approach
Section titled “Why this dual approach”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.
Current implementation status
Section titled “Current implementation status”| Aspect | Darwin (macOS) | NixOS |
|---|---|---|
| Clan vars | Planned (not yet implemented) | Active for system secrets |
| sops-nix | Active for user secrets | Active for user secrets |
| System secrets | Manual | Generated by clan vars |
| User secrets | sops-nix | sops-nix |
Clan vars is currently NixOS-only. Darwin support is planned but not yet implemented. User secrets on both platforms use sops-nix.
Step 1: Understand age encryption
Section titled “Step 1: Understand age encryption”Before setting up secrets, understand the encryption foundation.
What is age?
Section titled “What is age?”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.txtand decrypts your secrets - Your age public key is listed in
.sops.yamlso sops knows which keys can decrypt which files
Why derive from SSH keys?
Section titled “Why derive from SSH keys?”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.
Log into Bitwarden
Section titled “Log into Bitwarden”# Check if already logged inbw status
# If locked, unlockexport BW_SESSION=$(bw unlock --raw)
# If not logged in, login firstbw loginexport BW_SESSION=$(bw unlock --raw)Find your SSH key
Section titled “Find your SSH key”# List items containing "ssh" in the namebw list items --search "ssh" | jq '.[].name'
# Get the specific item IDbw list items --search "your-ssh-key-name" | jq '.[0].id'Export the private key
Section titled “Export the private key”# 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 accordinglySecurity note: We write to /tmp/ because this is temporary.
We’ll derive the age key and then securely delete the SSH key from disk.
Step 3: Derive age key from SSH key
Section titled “Step 3: Derive age key from SSH key”Now convert your SSH key to an age key using ssh-to-age.
Get the public key
Section titled “Get the public key”# Derive age public key from SSH public keyssh-to-age -i /tmp/ssh_key.pub# Output: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSave this output; you’ll need it for .sops.yaml.
Get the private key
Section titled “Get the private key”# Create the sops directorymkdir -p ~/.config/sops/age
# Derive age private key from SSH private keyssh-to-age -private-key -i /tmp/ssh_key > ~/.config/sops/age/keys.txt
# Set restrictive permissionschmod 600 ~/.config/sops/age/keys.txtVerify the key
Section titled “Verify the key”# Check the key file exists and has contenthead -1 ~/.config/sops/age/keys.txt# Should show: AGE-SECRET-KEY-1...
# Verify permissionsls -la ~/.config/sops/age/keys.txt# Should show: -rw------- (600)Clean up temporary files
Section titled “Clean up temporary files”# Securely delete the temporary SSH keyrm -P /tmp/ssh_key /tmp/ssh_key.pub 2>/dev/null || rm /tmp/ssh_key /tmp/ssh_key.pubStep 4: Add your key to .sops.yaml
Section titled “Step 4: Add your key to .sops.yaml”The .sops.yaml file tells sops which public keys can decrypt which secret files.
Understand the structure
Section titled “Understand the structure”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 - *crs58The structure defines:
- keys: Named age public keys (using YAML anchors like
&crs58) - creation_rules: Which keys can decrypt which file paths
Add your key
Section titled “Add your key”Add your age public key to the keys section:
keys: - &admin age1adminkey... - &crs58 age1crs58key... - &yourusername age1yourpublickey... # Add your keyThen add a creation rule for your secrets:
creation_rules: # Your user secrets - path_regex: secrets/users/yourusername/.*\.yaml$ key_groups: - age: - *admin - *yourusernameWhy 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.
Step 5: Create your first secret
Section titled “Step 5: Create your first secret”Now create an encrypted secrets file for your user.
Create the secrets directory
Section titled “Create the secrets directory”mkdir -p secrets/users/yourusernameCreate an encrypted file
Section titled “Create an encrypted file”sops secrets/users/yourusername/secrets.sops.yamlThis opens your $EDITOR with a new encrypted file.
Add your secrets in YAML format:
github-token: ghp_yourtokensome-api-key: sk-yourapikeySave and close.
Sops automatically encrypts the file using the keys specified in .sops.yaml.
Verify encryption
Section titled “Verify encryption”# View the raw encrypted filecat secrets/users/yourusername/secrets.sops.yamlYou’ll see the encrypted content with sops metadata. The values are encrypted; only the structure is visible.
Test decryption
Section titled “Test decryption”# Decrypt and viewsops -d secrets/users/yourusername/secrets.sops.yamlIf 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.
Configure sops in your user module
Section titled “Configure sops in your user module”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"; };}Understanding secret paths
Section titled “Understanding secret paths”When home-manager activates:
- sops-nix reads your encrypted file
- Decrypts it using your age key
- Writes each secret to
~/.config/sops-nix/secrets/<secret-name> - Sets appropriate permissions (default 0400, owner-read only)
You reference secrets via config.sops.secrets.<name>.path, which resolves to the decrypted file path.
Activate and verify
Section titled “Activate and verify”# Rebuild your configurationjust activate
# Check the secrets were deployedls -la ~/.config/sops-nix/secrets/
# Verify a secret's content (carefully!)cat ~/.config/sops-nix/secrets/github-tokenWhat you’ve learned
Section titled “What you’ve learned”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
Next steps
Section titled “Next steps”Now that you understand secrets:
-
Add more secrets as needed. Use templates for complex formats; see the Secrets Management Guide.
-
Understand clan vars for the target architecture. Clan vars are generated automatically, and understanding them helps with migration planning.
-
Continue to platform-specific tutorials:
- Darwin Deployment Tutorial for macOS specifics
- NixOS Deployment Tutorial for server deployment
-
Review operational procedures in the Secrets Management Guide for rotation, sharing, and troubleshooting.
Troubleshooting
Section titled “Troubleshooting””No secret key found” during decryption
Section titled “”No secret key found” during decryption”Your age key isn’t where sops expects it:
# Check the key file existsls -la ~/.config/sops/age/keys.txt
# Verify it's a valid age keyhead -1 ~/.config/sops/age/keys.txt # Should start with AGE-SECRET-KEY-“could not decrypt” error
Section titled ““could not decrypt” error”Your age key doesn’t match any keys in .sops.yaml:
# Get your age public keyage-keygen -y ~/.config/sops/age/keys.txt
# Compare with what's in .sops.yamlgrep "your-expected-prefix" .sops.yamlIf they don’t match, either update .sops.yaml or regenerate your age key.
Secrets not appearing after activation
Section titled “Secrets not appearing after activation”Check sops-nix configuration:
# Verify the sops configuration is being evaluatednix eval .#homeConfigurations.yourusername.config.sops.secrets --json | jq
# Check home-manager logs during activationhome-manager switch --flake .#yourusername --show-trace 2>&1 | grep -i sopsPermission denied on secret file
Section titled “Permission denied on secret file”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.