SOPS Secret Management with Ledger
This guide explains how to use SOPS (Secrets OPerationS) for secure secret management in your Nix configuration, using your Ledger hardware wallet for encryption and decryption.
Overview
Section titled “Overview”SOPS is a tool for managing secrets in Git repositories. It encrypts files using GPG, age, or cloud KMS, allowing you to safely commit encrypted secrets to version control.
Why SOPS?
Section titled “Why SOPS?”- Git-friendly: Encrypted secrets can be safely committed to repositories
- Hardware security: Uses your Ledger GPG key for encryption/decryption
- Declarative: Integrates with Nix for reproducible secret deployment
- Selective encryption: Only encrypts values, not keys (readable structure)
- Multi-format: Supports YAML, JSON, ENV, INI, and binary files
Security Model
Section titled “Security Model”In this configuration:
- Encryption key: Stored on Ledger hardware (never exposed to computer)
- Physical confirmation: Every decrypt operation requires Ledger button press
- Git safety: Only encrypted data is committed to repository
- Backup: Keys recoverable via Ledger’s 24-word recovery phrase
Architecture
Section titled “Architecture”┌─────────────────────────────────────────────────────────────┐│ Your Workflow │├─────────────────────────────────────────────────────────────┤│ 1. Edit secret: sops secrets.yaml ││ 2. SOPS decrypts using Ledger GPG key ││ 3. Opens in editor (plaintext in memory only) ││ 4. You save changes ││ 5. SOPS re-encrypts with Ledger GPG key ││ 6. Safe to commit encrypted file │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ Encryption Flow │├─────────────────────────────────────────────────────────────┤│ ││ Plain text secret ││ │ ││ ▼ ││ SOPS encrypts ──────> Ledger GPG signs ││ │ │ ││ │ ▼ ││ │ [Confirm on device] ││ │ │ ││ ▼ ▼ ││ Encrypted file (safe for Git) ││ │└─────────────────────────────────────────────────────────────┘
Setup (Already Configured)
Section titled “Setup (Already Configured)”Your Nix configuration includes:
1. Flake Inputs
Section titled “1. Flake Inputs”inputs = { sops-nix = { url = "github:Mic92/sops-nix"; inputs.nixpkgs.follows = "nixpkgs"; };};
2. Darwin Module
Section titled “2. Darwin Module”darwinConfigurations."wikigen-mac" = darwin.lib.darwinSystem { modules = [ sops-nix.darwinModules.sops # ... ];};
3. SOPS Configuration (nix/modules/secrets/sops.nix)
Section titled “3. SOPS Configuration (nix/modules/secrets/sops.nix)”sops = { gnupg.home = "~/.gnupg-ledger"; defaultSopsFile = ./nix/secrets/secrets.yaml;};
environment.variables = { GNUPGHOME = "/Users/wikigen/.gnupg-ledger";};
4. SOPS Rules (.sops.yaml)
Section titled “4. SOPS Rules (.sops.yaml)”creation_rules: - path_regex: .*\.(yaml|json|env|ini)$ pgp: >- D2A7EC63E350CC488197CB2ED369B07E00FB233E
Directory Structure
Section titled “Directory Structure”Config/├── .sops.yaml # SOPS configuration├── nix/│ └── secrets/│ ├── README.md # Usage documentation│ ├── secrets.yaml.example # Template│ ├── secrets.yaml # Encrypted secrets (safe to commit)│ └── test-secret.yaml # Test file└── docs/ └── sops.md # This guide
Environment Setup
Section titled “Environment Setup”Before using SOPS, ensure your environment is configured:
# Set GPG home directory to Ledger keyringexport GNUPGHOME=/Users/wikigen/.gnupg-ledger
# Verify ledger-gpg-agent is runningpgrep -f ledger-gpg-agent
# If not running, start it:ledger-gpg-agent --homedir ~/.gnupg-ledger --server --verbose &
Pro tip: Add to your ~/.zshrc
:
export GNUPGHOME=/Users/wikigen/.gnupg-ledger
Creating a New Secret File
Section titled “Creating a New Secret File”Method 1: From Example Template
Section titled “Method 1: From Example Template”# Copy the examplecp nix/secrets/secrets.yaml.example nix/secrets/my-secrets.yaml
# Edit and encrypt in one stepsops nix/secrets/my-secrets.yaml
Method 2: Create from Scratch
Section titled “Method 2: Create from Scratch”# SOPS will create an empty file, open editor, then encryptsops nix/secrets/my-secrets.yaml
What happens:
- SOPS contacts your Ledger via
ledger-gpg-agent
- Ledger displays “Sign message”
- Press button to confirm
- Editor opens with decrypted content
- Make your changes and save
- SOPS re-encrypts on save
Editing Encrypted Secrets
Section titled “Editing Encrypted Secrets”sops nix/secrets/secrets.yaml
SOPS will:
- Decrypt the file (requires Ledger confirmation)
- Open in your
$EDITOR
(defaults tovim
) - Re-encrypt when you save and exit
Viewing Secrets
Section titled “Viewing Secrets”# Decrypt and print to stdoutsops -d nix/secrets/secrets.yaml
# View specific keysops -d --extract '["example"]["api_key"]' nix/secrets/secrets.yaml
Different File Formats
Section titled “Different File Formats”# JSONsops secrets.json
# Environment filesops .env
# INI filesops config.ini
# Binary filesops --input-type binary --output-type binary secret.bin
Encrypting an Existing File
Section titled “Encrypting an Existing File”# In-place encryptionsops --encrypt --in-place secrets.yaml
# Create encrypted copysops --encrypt secrets.yaml > secrets.enc.yaml
Using Secrets in Nix
Section titled “Using Secrets in Nix”Basic Secret Declaration
Section titled “Basic Secret Declaration”In your host configuration (e.g., hosts/wikigen-mac.nix
):
{ # Declare a secret sops.secrets."example/api_key" = {};
# The secret will be available at: # /run/secrets/example/api_key}
Custom Secret Path
Section titled “Custom Secret Path”sops.secrets."example/api_key" = { path = "/run/secrets/my_api_key";};
Secret with Specific Owner/Mode
Section titled “Secret with Specific Owner/Mode”sops.secrets."database/password" = { owner = "wikigen"; mode = "0400"; # Read-only for owner};
Using Secrets in Services
Section titled “Using Secrets in Services”{ # Declare the secret sops.secrets."app/jwt_secret" = {};
# Reference in a launchd service launchd.user.agents.myapp = { config = { ProgramArguments = [ "${pkgs.myapp}/bin/myapp" "--jwt-secret-file" config.sops.secrets."app/jwt_secret".path ]; }; };}
Using Secrets in Home Manager
Section titled “Using Secrets in Home Manager”In your user config (e.g., home/users/wikigen.nix
):
{ config, ... }:{ # Create a config file with secret home.file.".config/myapp/config.json".text = builtins.toJSON { api_key_file = config.sops.secrets."example/api_key".path; };}
Environment Variables from Secrets
Section titled “Environment Variables from Secrets”{ sops.secrets."aws/credentials" = {};
# Read secret content into environment environment.variables = { AWS_CREDENTIALS_FILE = config.sops.secrets."aws/credentials".path; };}
Secret File Format
Section titled “Secret File Format”SOPS supports nested YAML structures:
# Development secretsdev: database: host: localhost port: 5432 password: dev_password_here api_key: dev_api_key_here
# Production secretsprod: database: host: prod-db.example.com port: 5432 password: prod_password_here api_key: prod_api_key_here
# AWS credentialsaws: access_key_id: AKIAIOSFODNN7EXAMPLE secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Application secretsapp: jwt_secret: super-secret-jwt-signing-key-32-bytes encryption_key: another-32-byte-encryption-key
After encryption, this becomes:
dev: database: host: ENC[AES256_GCM,data:bG9jYWxob3N0,iv:...] port: ENC[AES256_GCM,data:NTQzMg==,iv:...] password: ENC[AES256_GCM,data:ZGV2X3Bhc3N3b3JkX2hlcmU=,iv:...] api_key: ENC[AES256_GCM,data:ZGV2X2FwaV9rZXlfaGVyZQ==,iv:...]sops: pgp: - fp: D2A7EC63E350CC488197CB2ED369B07E00FB233E # ... metadata ...
Notice:
- Structure is visible: You can see keys and organization
- Values are encrypted: Actual secrets are protected
- Git-diffable: Changes show which values changed, not content
Advanced Usage
Section titled “Advanced Usage”Multiple GPG Keys
Section titled “Multiple GPG Keys”To allow multiple people to decrypt secrets:
creation_rules: - path_regex: .*\.yaml$ pgp: >- D2A7EC63E350CC488197CB2ED369B07E00FB233E, ANOTHERKEY1234567890ABCDEF1234567890ABCDEF
Rotating Keys
Section titled “Rotating Keys”If you need to change encryption keys:
# Update .sops.yaml with new key(s)# Then rotate all secretssops updatekeys nix/secrets/secrets.yaml
This re-encrypts the file with the new key configuration.
Different Keys for Different Files
Section titled “Different Keys for Different Files”creation_rules: # Development secrets - path_regex: secrets/dev/.*\.yaml$ pgp: >- D2A7EC63E350CC488197CB2ED369B07E00FB233E
# Production secrets (different key) - path_regex: secrets/prod/.*\.yaml$ pgp: >- PRODUCTIONKEY1234567890ABCDEF1234567890AB
Extracting Specific Keys
Section titled “Extracting Specific Keys”# Get a single valuesops -d --extract '["database"]["password"]' secrets.yaml
# Use in scriptsDB_PASSWORD=$(sops -d --extract '["database"]["password"]' secrets.yaml)
Encrypting Specific Keys Only
Section titled “Encrypting Specific Keys Only”creation_rules: - path_regex: config\.yaml$ encrypted_regex: '^(password|api_key|secret)$'
Only keys matching the regex will be encrypted.
Integration with Git
Section titled “Integration with Git”What to Commit
Section titled “What to Commit”✅ DO commit:
.sops.yaml
(configuration)- Encrypted secret files (
secrets.yaml
) nix/secrets/README.md
nix/secrets/secrets.yaml.example
❌ DON’T commit:
- Unencrypted secrets
- GPG private keys
secrets.yaml.example
with real secrets
Git Configuration
Section titled “Git Configuration”Add to .gitignore
if you have temporary unencrypted files:
# Temporary unencrypted secrets*.dec.yaml*.dec.json*_decrypted.*
Pre-commit Hook
Section titled “Pre-commit Hook”To ensure secrets are always encrypted:
repos: - repo: https://github.com/getsops/sops rev: v3.8.1 hooks: - id: sops-check
Troubleshooting
Section titled “Troubleshooting””No GPG key found”
Section titled “”No GPG key found””Problem: SOPS can’t find your Ledger GPG key.
Solution:
# Ensure GNUPGHOME points to Ledger keyringexport GNUPGHOME=/Users/wikigen/.gnupg-ledger
# Verify key is visiblegpg --list-keys
# Should show: D2A7EC63E350CC488197CB2ED369B07E00FB233E
“Failed to get data key”
Section titled ““Failed to get data key””Problem: SOPS can’t decrypt with your key.
Solution:
# Check .sops.yaml has correct key fingerprintcat .sops.yaml
# Verify fingerprint matches your keygpg --list-keys --fingerprint
# Ensure ledger-gpg-agent is runningpgrep -f ledger-gpg-agent || \ ledger-gpg-agent --homedir ~/.gnupg-ledger --server --verbose &
“End of file” Error
Section titled ““End of file” Error”Problem: Agent can’t communicate with Ledger.
Solution:
- Check Ledger is connected and unlocked
- Open “SSH/GPG Agent” app on Ledger
- Screen should show “SSH/GPG Agent is ready”
- Restart agent:
Terminal window killall ledger-gpg-agentledger-gpg-agent --homedir ~/.gnupg-ledger --server --verbose &sleep 2
“Could not decrypt”
Section titled ““Could not decrypt””Problem: File encrypted with different key.
Solution:
# Check which keys can decrypt the filesops -d --verbose secrets.yaml
# Re-encrypt with your current keysops updatekeys secrets.yaml
Ledger Not Responding
Section titled “Ledger Not Responding”Checklist:
- Ledger connected via USB
- Ledger unlocked (PIN entered)
- SSH/GPG Agent app is open on device
- Screen shows “SSH/GPG Agent is ready”
-
ledger-gpg-agent
process is running
Test:
# Try a simple GPG operationecho "test" | gpg --clearsign# Should prompt Ledger for confirmation
SOPS Opens Wrong Editor
Section titled “SOPS Opens Wrong Editor”Problem: SOPS opens vim
but you want a different editor.
Solution:
# Set editor in shell config (~/.zshrc)export EDITOR="nano" # or "code --wait", "emacs", etc.
# Or set for single commandEDITOR=nano sops secrets.yaml
Best Practices
Section titled “Best Practices”Secret Organization
Section titled “Secret Organization”Good structure:
# Group by environment and servicedev: database: password: xxx redis: password: xxx
prod: database: password: xxx redis: password: xxx
Avoid:
# Flat, unclear structuredb_password: xxxredis_pw: xxxprod_db_password: xxx
Secret Naming
Section titled “Secret Naming”- Use descriptive names:
database_password
notdbpw
- Include context:
prod/api_key
not justkey
- Be consistent:
api_key
everywhere, not mixing withapiKey
File Organization
Section titled “File Organization”nix/secrets/├── common.yaml # Shared across all environments├── development.yaml # Dev-only secrets├── production.yaml # Prod-only secrets└── personal.yaml # Your personal API keys
Secret Rotation
Section titled “Secret Rotation”- Generate new secret value
- Update in SOPS file:
sops secrets.yaml
- Rebuild system:
darwin-rebuild switch --flake .#wikigen-mac
- Verify new secret works
- Revoke old secret in external system
Backup Strategy
Section titled “Backup Strategy”Your secrets are encrypted with your Ledger GPG key. To ensure access:
- Ledger backup: Securely store your 24-word recovery phrase
- Alternative access: Consider adding a second GPG key (different Ledger or software key)
- Test recovery: Periodically verify you can recover keys from seed
Security Notes
Section titled “Security Notes”Threat Model
Section titled “Threat Model”What SOPS protects against:
- ✅ Secrets leaked in Git repository
- ✅ Secrets readable on disk
- ✅ Secrets accidentally committed to public repos
What SOPS doesn’t protect against:
- ❌ Malware on your computer (can read decrypted secrets in memory)
- ❌ Physical access to running system (secrets in /run/secrets/)
- ❌ Compromise of Ledger device itself
Hardware Security
Section titled “Hardware Security”- Private key never leaves Ledger: Only signs/decrypts on device
- Physical confirmation required: Must press button for every operation
- No remote exploitation: Can’t decrypt without physical device
- Recovery: Keys derived from 24-word seed (keep secure!)
Operational Security
Section titled “Operational Security”- Lock your screen when away from computer
- Remove Ledger when not actively using SOPS
- Audit secret access via Git history
- Rotate secrets periodically
- Use unique secrets per environment
Quick Reference
Section titled “Quick Reference”Common Commands
Section titled “Common Commands”# Setup environmentexport GNUPGHOME=/Users/wikigen/.gnupg-ledger
# Create/edit secretsops secrets.yaml
# View secretsops -d secrets.yaml
# Extract valuesops -d --extract '["key"]["subkey"]' secrets.yaml
# Encrypt existing filesops --encrypt --in-place file.yaml
# Rotate keyssops updatekeys secrets.yaml
# Check which keys can decryptsops -d --verbose secrets.yaml
File Locations
Section titled “File Locations”- Configuration:
.sops.yaml
- Secrets directory:
nix/secrets/
- GPG keyring:
~/.gnupg-ledger/
- Agent logs:
~/.local/share/ledger-gpg-agent.log
- Decrypted secrets:
/run/secrets/
(runtime only)
Key Information
Section titled “Key Information”- Fingerprint:
D2A7EC63E350CC488197CB2ED369B07E00FB233E
- Identity: Luc Chartier luc@distorted.media
- Algorithm: ECDSA (NIST P-256)
- Storage: Ledger Nano S hardware wallet
Related Documentation
Section titled “Related Documentation”- Ledger Setup Guide - Hardware wallet configuration
- Ledger Deep Dive - Comprehensive Ledger guide
- GPG Signing - GPG configuration and commit signing
- Design Doc - Overall Nix configuration architecture
- Structure Guide - Modular configuration explained
External References
Section titled “External References”- SOPS GitHub - Official SOPS repository
- sops-nix - NixOS integration
- SOPS Guide - Official usage guide
- Mozilla SOPS - Original announcement
Next Steps
Section titled “Next Steps”Now that SOPS is configured:
-
Create your first real secret:
Terminal window sops nix/secrets/secrets.yaml -
Use it in your config:
sops.secrets."my-app/api-key" = {}; -
Rebuild your system:
Terminal window darwin-rebuild switch --flake .#wikigen-mac -
Verify secret is available:
Terminal window ls -l /run/secrets/
Happy secret managing! 🔐