Skip to content

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.

cp dot_secrets.example ~/.secrets
chmod 600 ~/.secrets # Secure permissions
nvim ~/.secrets

To safely backup your secrets to the private git repository:

mise run setup-secrets

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

  1. Principle of Least Privilege: Only deploy secrets that the specific server actually needs
  2. Ephemeral Keys: For CI/CD, prefer environment variables over persisted files
  3. Audit Logging: Use vault audit logs to track when/where age keys are accessed
  4. Key Rotation: Rotate the age key after decommissioning servers that had access
  5. 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:

[ -f ~/.secrets ] && echo "Secrets applied" || echo "Secrets skipped"

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):

git commit --no-verify  # Skip gitleaks check

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

mise run rotate-secrets

This will:

  1. Generate a new age key
  2. Decrypt your secrets with the old key
  3. Re-encrypt with the new key
  4. 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:

bw-get "AWS Prod Root"
# Outputs: <your-secret>

2. Run a command with a secret (never stored in history):

bw-env AWS_SECRET_ACCESS_KEY "AWS Prod Root" aws s3 ls

3. Unlock once, use multiple times:

bw-unlock  # Enter master password once
bw-get "GitHub Token"
bw-get "OpenAI Key"

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.

# Unlock Bitwarden session
# Follow the instructions to eval the session key
mise run bw-login

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.

# Login to Vault
export VAULT_ADDR="https://vault.example.com"
mise run vault-login

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.