Secrets Management¶
This repository uses a hybrid approach for secrets: 1. Local Files: Unencrypted ~/.secrets file for active use in your shell. 2. Encrypted Storage: age-encrypted encrypted_dot_secrets.age file for safe backup in the git repository.
Getting Started¶
1. Manual Setup¶
Copy the template and edit it locally. This file is ignored by git and will never be committed unencrypted.
2. Encryption (Recommended)¶
To safely backup your secrets to the private git repository:
This task will: 1. Generate an age key at ~/.config/chezmoi/key.txt (if missing). 2. Configure chezmoi to use this key. 3. Encrypt ~/.secrets to encrypted_dot_secrets.age.
Once encrypted, you can commit encrypted_dot_secrets.age to the repository.
Server Deployment¶
When deploying dotfiles to servers, secrets management requires special consideration since servers typically: - Don't have access to your local age encryption key - May need secrets injected from a central vault (HashiCorp Vault, AWS Secrets Manager, etc.) - Often run in non-interactive environments (CI/CD, containers)
Option 1: No Encrypted Secrets (Default)¶
If you skip encrypted files during deployment, chezmoi will simply ignore encrypted_dot_secrets.age:
# Server deployment without secrets
chezmoi init --apply jerdaw/dotfiles --exclude "run_once_*"
# Result: Dotfiles applied, encrypted files skipped, no errors
When to use: Servers that don't need the secrets in ~/.secrets (e.g., build servers, bastion hosts).
Option 2: Pre-Inject Age Key from Vault¶
For servers that need encrypted secrets, inject the age key from your secrets vault before applying dotfiles:
# Example: Fetch age key from HashiCorp Vault
vault kv get -field=key secret/dotfiles/age-key > ~/.config/chezmoi/key.txt
chmod 600 ~/.config/chezmoi/key.txt
# Now apply dotfiles (encrypted files will decrypt)
chezmoi init --apply jerdaw/dotfiles --exclude "run_once_*"
AWS Secrets Manager Example:
aws secretsmanager get-secret-value \
--secret-id dotfiles/age-key \
--query SecretString \
--output text > ~/.config/chezmoi/key.txt
chmod 600 ~/.config/chezmoi/key.txt
Azure Key Vault Example:
az keyvault secret show \
--vault-name my-vault \
--name dotfiles-age-key \
--query value -o tsv > ~/.config/chezmoi/key.txt
chmod 600 ~/.config/chezmoi/key.txt
Option 3: Environment Variable Injection¶
For ephemeral environments (CI, Docker containers), inject secrets via environment variables instead of files:
# In CI/CD pipeline
export OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" # GitHub Actions
export ANTHROPIC_API_KEY="${VAULT_ANTHROPIC_KEY}" # From vault
# Apply dotfiles without encrypted secrets
chezmoi init --apply jerdaw/dotfiles --exclude "run_once_*"
# Secrets are already in environment, no ~/.secrets file needed
Option 4: Server-Specific Secrets File¶
Create a minimal ~/.secrets file manually on the server:
# After applying dotfiles
cat > ~/.secrets <<'EOF'
# Server-specific secrets (DO NOT commit)
export DATABASE_URL="postgresql://..."
export API_KEY="server-specific-key"
EOF
chmod 600 ~/.secrets
Automated Server Provisioning Example¶
Terraform + HashiCorp Vault:
resource "null_resource" "dotfiles" {
provisioner "remote-exec" {
inline = [
# Install chezmoi
"sh -c \"$(curl -fsLS get.chezmoi.io)\"",
# Inject age key from Vault
"vault kv get -field=key secret/dotfiles/age-key > ~/.config/chezmoi/key.txt",
"chmod 600 ~/.config/chezmoi/key.txt",
# Apply dotfiles
"chezmoi init --apply jerdaw/dotfiles --exclude 'run_once_*'",
]
}
}
Best Practices for Server Secrets¶
- Principle of Least Privilege: Only deploy secrets that the specific server actually needs
- Ephemeral Keys: For CI/CD, prefer environment variables over persisted files
- Audit Logging: Use vault audit logs to track when/where age keys are accessed
- Key Rotation: Rotate the age key after decommissioning servers that had access
- Immutable Infrastructure: For containers, bake secrets at build time or inject at runtime via orchestrator (Kubernetes secrets, Docker secrets)
Troubleshooting¶
Q: Chezmoi says "age key not found" on server A: This is expected if you didn't inject the key. Encrypted files are skipped without error.
Q: How do I know if encrypted files were applied? A: Check if ~/.secrets exists and contains decrypted content:
Q: Should I store the age key in my infrastructure-as-code repo? A: NO. Always fetch from a secrets vault at runtime. Never commit keys to git, even in private repos.
Security Model¶
File Permissions¶
The age key (~/.config/chezmoi/key.txt) and your local secrets (~/.secrets) are secured using User Ownership with 0600 permissions (rx-------). - Owned by: Your user ($USER). - Access: Only your user can read/write. - Root: Root can access technically, but changing ownership to root would break your tooling (chezmoi/mise running as user).
Environment Variable Risk ⚠️¶
By default, ~/.secrets is sourced into ~/.zshrc. This means every secret in that file is loaded into the environment variables of every terminal shell you open.
Risk: Any malicious script, npm package, or compromised tool running in your shell can read these variables (process memory/environment).
Mitigation: - Minimize Scope: Only put secrets in ~/.secrets that you truly need globally (e.g., OPENAI_API_KEY for CLI tools). - Use Tools: For project-specific secrets, use tools like .env files, direnv, or mise configuration per-project, rather than global exports.
Git Hooks Protection¶
The repository includes gitleaks integration in the global pre-commit hook to prevent accidental secret commits.
How it works: - Pre-commit hook scans staged files for secrets before commit - Blocks commits if potential secrets are detected - Runs automatically on every git commit
Example output:
$ git commit -m "Add config"
🔍 Scanning for secrets...
❌ Gitleaks detected potential secrets.
To bypass this check (use with caution):
git commit --no-verify
Bypass (emergency only):
Installation:
# Install gitleaks if not available
mise use -g gitleaks@latest
# Verify hook is active
mise run doctor # Check for "✓ pre-commit hook (executable)"
Note: Gitleaks also runs in CI for additional protection. See .github/workflows/ci.yml.
What to Store¶
✅ Safe to Store (Low-Medium Risk)¶
- Development API keys (OpenAI, GitHub, Anthropic).
- Local app configuration (Internal DB URLs).
- Personal automation tokens (Weather, Music).
❌ Do Not Store (High Risk)¶
Use a password manager (1Password, Bitwarden) or Hardware Key (YubiKey) for these: - Production Root Credentials (AWS Root, Admin keys). - Financial Secrets (Banking, Crypto seed phrases). - Identity Documents (Scans of IDs).
Key Rotation¶
For security best practices, rotate your age encryption key periodically (recommended: annually) or immediately if you suspect compromise.
Automated Rotation¶
This will:
- Generate a new age key
- Decrypt your secrets with the old key
- Re-encrypt with the new key
- Backup the old key with timestamp
Important
After rotation, update your password manager with the new key!
Manual Rotation¶
If you need to rotate manually:
# 1. Backup old key
cp ~/.config/chezmoi/key.txt ~/.config/chezmoi/key.txt.bak
# 2. Generate new key
age-keygen -o ~/.config/chezmoi/key.new.txt
# 3. Re-encrypt secrets
age -d -i ~/.config/chezmoi/key.txt encrypted_dot_secrets.age | \
age -e -a -R ~/.config/chezmoi/key.new.txt -o encrypted_dot_secrets.age
# 4. Replace old key
mv ~/.config/chezmoi/key.new.txt ~/.config/chezmoi/key.txt
When to Rotate¶
- Scheduled: At least once per year
- Immediately if:
- You suspect key compromise
- A machine with the key was lost/stolen
- A backup was exposed
- You're leaving a job/project
The doctor task will warn you if your key is older than 1 year.
Advanced Secrets (On-Demand via Bitwarden)¶
For high-value secrets that should never sit in your shell environment, use the Bitwarden CLI to fetch them only when needed.
Available Helpers¶
| Command | Description |
|---|---|
bw-unlock | Unlock Bitwarden for this terminal session |
bw-get <item> | Fetch a password by item name (auto-unlocks) |
bw-env <VAR> <item> <cmd> | Run a command with a secret injected |
Usage Examples¶
1. Fetch a secret directly:
2. Run a command with a secret (never stored in history):
3. Unlock once, use multiple times:
Why This is Safer¶
- No Global Exposure: Secret is only in the subprocess environment, not your shell.
- No History Leak: The secret value never appears in your command history.
- Vault Locked by Default: Bitwarden stays locked until you explicitly unlock it.
Alternative: mise Secrets Integration¶
For declarative, project-scoped secrets, mise can integrate with Bitwarden, 1Password, and Vault.
Bitwarden Integration¶
Ensure bw is installed (mise install) and logged in.
Usage in mise.toml:
[env]
# Requires "bitwarden-cli" to be installed
# Using the "bitwarden" backend (standard in mise)
AWS_SECRET_ACCESS_KEY = { source = "bitwarden", item = "AWS Prod", field = "password" }
HashiCorp Vault Integration¶
Ensure vault is installed and you are logged in.
Usage in mise.toml:
[env]
# Requires "vault" CLI
DB_PASSWORD = { source = "vault", key = "secret/data/db/prod", field = "password" }
# mise.toml (project-level)
[env]
AWS_SECRET_ACCESS_KEY = { source = "bitwarden", item = "AWS Prod", field = "password" }
This injects the secret only when running mise tasks, not globally.
Related Documentation¶
- Troubleshooting - Common secrets-related problems
- Server Deployment - Secrets in server mode
- Security Policy - Security model and vulnerability reporting