This guide covers security best practices and emergency procedures for managing encrypted dotfiles with age and chezmoi.
This repository uses a two-layer encryption model to protect sensitive data:
key.txt.age (in repository, password-protected)
↓ Decrypt with password from 1Password
~/key.txt (local age identity/private key)
↓ Decrypt other encrypted files
encrypted_*.age (SSH config, Google IME dictionary, etc.)
Key Points:
key.txt.ageis stored in the repository, encrypted with a password (scrypt)- The password is stored securely in 1Password
- Only those with the password can extract the age private key
- The age private key (
~/key.txt) is used to decrypt other encrypted files - NEVER commit
~/key.txtto the repository
key.txt.age- Password-protected age private keyencrypted_*.age- Age-encrypted sensitive files (SSH configs, etc.)- All encryption uses the age tool with strong cryptographic primitives
IMPORTANT: This procedure is for emergencies only (key leak, device compromise, etc.). Do NOT perform routine key rotation unless necessary.
Rotate your age key immediately if:
- Your
~/key.txtfile is accidentally committed to a public repository - Your device is lost, stolen, or compromised
- You suspect unauthorized access to your encrypted files
- You accidentally shared your age private key
# Generate new age key pair
age-keygen --output ~/key.txt.new
# Backup the old key temporarily (in case rotation fails)
cp ~/key.txt ~/key.txt.backupExtract the new public key:
grep "^# public key: " ~/key.txt.newNavigate to your chezmoi source directory:
cd ~/.local/share/chezmoiRe-encrypt key.txt.age:
# Create a secure temporary directory
TMPDIR="$(mktemp -d)"
chmod 700 "$TMPDIR"
# Encrypt the NEW private key with a password
# Note: key.txt.age is password-protected, not recipient-encrypted
age -p -o key.txt.age.new ~/key.txt.new
# Enter a strong password (store in 1Password immediately!)
# Verify the new encrypted file works (with error handling)
# This will prompt for the password you just set
age -d -o "$TMPDIR/test_decrypt.txt" key.txt.age.new
diff ~/key.txt.new "$TMPDIR/test_decrypt.txt" || {
echo "ERROR: Re-encrypted file verification failed!"
rm -rf "$TMPDIR"
exit 1
}
# Replace old with new
mv key.txt.age.new key.txt.age
# Clean up the temporary directory
rm -rf "$TMPDIR"Re-encrypt other .age files:
# Create a secure temporary directory
TMPDIR="$(mktemp -d)"
chmod 700 "$TMPDIR"
# Find all .age files (excluding key.txt.age)
git ls-files '*.age' | grep -v '^key\.txt\.age$'
# Extract and validate the new public key
NEW_PUBLIC_KEY=$(grep "^# public key: " ~/key.txt.new | sed 's/^# public key: //')
if [ -z "$NEW_PUBLIC_KEY" ]; then
echo "ERROR: Could not extract public key from ~/key.txt.new" >&2
rm -rf "$TMPDIR"
exit 1
fi
# For each file, decrypt with old key and re-encrypt with new key
# Example for Google IME dictionary:
age -d -i ~/key.txt.backup \
-o "$TMPDIR/temp_decrypted.txt" \
private_dot_config/google_ime/encrypted_google_ime_dictionary.txt.age
age -r "$NEW_PUBLIC_KEY" \
-o private_dot_config/google_ime/encrypted_google_ime_dictionary.txt.age.new \
"$TMPDIR/temp_decrypted.txt"
# Verify decryption works with new key
age -d -i ~/key.txt.new -o /dev/null private_dot_config/google_ime/encrypted_google_ime_dictionary.txt.age.new
# Replace old with new
mv private_dot_config/google_ime/encrypted_google_ime_dictionary.txt.age.new \
private_dot_config/google_ime/encrypted_google_ime_dictionary.txt.age
# Clean up the temporary directory
rm -rf "$TMPDIR"# Replace old key with new key
mv ~/key.txt.new ~/key.txt
chmod 600 ~/key.txt
# Test that chezmoi can decrypt files
chezmoi diffcd ~/.local/share/chezmoi
# Verify no plaintext keys are being committed
git status
git diff
# Commit the re-encrypted files
git add key.txt.age
git add private_dot_config/google_ime/encrypted_google_ime_dictionary.txt.age
# Add any other re-encrypted .age files
git commit -m "security: rotate age encryption key
Re-encrypted all .age files with new age key due to [reason].
- Generated new age key pair
- Re-encrypted key.txt.age with new password
- Re-encrypted all sensitive files
"
git push# Securely delete old key backup
rm -f ~/key.txt.backup
# Update password in 1Password
# Store the new password for key.txt.age in 1Password- All
.agefiles re-encrypted with new key -
chezmoi diffworks without errors - Changes committed and pushed to GitHub
- New password stored in 1Password
- Old key backup deleted
- Test recovery on different machine (optional but recommended)
This repository includes automated security checks that run on every push and pull request.
The GitHub Actions workflow (.github/workflows/security-checks.yml) performs:
-
Plaintext Key Detection
- Prevents accidental commit of
~/key.txt(unencrypted private key) - Checks for common key file naming patterns
- PASS: No plaintext key files found
- FAIL: Plaintext key detected in commit
- Prevents accidental commit of
-
Age Encryption Verification
- Verifies
key.txt.ageexists in repository - Validates all
.agefiles are properly encrypted - Checks file format headers (
age-encryption.org/v1or-----BEGIN AGE ENCRYPTED FILE-----) - PASS: All files properly encrypted
- FAIL: Missing or corrupted .age files
- Verifies
-
Secret Pattern Detection
- Scans for common secrets using pattern matching
- Detects: AWS keys, private keys, GitHub tokens, API keys
- Excludes: Encrypted files, documentation examples
- PASS: No secrets detected
- FAIL: Potential secrets found
The workflow uses:
- ripgrep - Fast pattern matching for secret detection
- Shell scripts - Lightweight checks without external dependencies
- No password required - CI cannot decrypt files (password not stored)
- Cannot verify decryption (no password in CI)
- Pattern matching may have false positives
- Only catches common secret patterns
- Manual review still important for sensitive changes
- Review the error message - Identifies which check failed
- Remove sensitive data if detected
- Fix corrupted .age files if encryption check failed
- Verify you didn't commit
~/key.txt(plaintext key)
If you believe it's a false positive, review the patterns in the workflow file.
Git history serves as the audit trail for encrypted files:
- Change history:
git log --follow -- '*.age'tracks all changes - Diff check:
git diffshows which files changed (content is encrypted) - Commit messages: Document reasons for important changes
# All .age file changes
git log --oneline --name-only -- '*.age'
# Specific file history
git log --follow -- key.txt.age
# Recent changes with dates
git log --pretty=format:"%h %ad %s" --date=short -- '*.age'- Meaningful commit messages - Explain why encrypted files changed
- Separate commits - Don't mix encrypted file changes with other changes
- Review before push - Always check
git diffbefore pushing
-
Never commit plaintext keys
- Keep
~/key.txtoutside git-tracked directories - Only commit
key.txt.age(password-protected) - CI will catch accidental commits
- Keep
-
Store passwords securely
- Use 1Password for the
key.txt.agepassword - Enable 2FA on 1Password
- Keep Emergency Kit in secure physical location
- Use 1Password for the
-
Minimize exposure
- Only decrypt when needed
- Use secure temporary directories (
mktemp -dwithchmod 700) - Clean up decrypted files immediately
-
Regular backups
- 1Password Emergency Kit (printed, in safe)
- GitHub repository (encrypted files)
- See backup-restore.md for details
# Always review what you're committing
git status
git diff
# Check for secrets locally (requires ripgrep)
# Use same exclusions as CI for consistent results
rg -i 'AKIA[0-9A-Z]{16}' --type-not lock --type-not svg -g '!*.age' -g '!**/security.md' .
rg -e '-----BEGIN.*PRIVATE KEY-----' --type-not lock --type-not svg -g '!*.age' -g '!**/security.md' .
# Verify encrypted files
git ls-files '*.age' | while IFS= read -r f; do
head -n 1 "$f" | grep -qE 'age-encryption.org|BEGIN AGE ENCRYPTED' && echo "OK: $f" || echo "ERROR: $f"
done-
Rotation Policy
- No routine rotation required for personal dotfiles
- Rotate only in emergencies (see Emergency Key Rotation)
- Document reason for rotation in commit message
-
Access Control
- Keep
key.txt.agepassword to yourself - Don't share age private key (
~/key.txt) - Review repository access regularly
- Keep
-
Audit Trail
- Git history tracks all changes to encrypted files
- Commit messages should explain sensitive changes
- Monitor notifications for unexpected changes
-
New machine checklist
- Clone repository via SSH (not HTTPS)
- Decrypt
key.txt.ageto~/key.txt - Set permissions:
chmod 600 ~/key.txt - Verify:
chezmoi diffworks - See backup-restore.md for full setup
-
Machine retirement
- Securely delete
~/key.txt - Clear shell history if it contains passwords
- Consider key rotation if machine was compromised
- Securely delete
- Age encryption tool
- Chezmoi documentation
- Backup and Restore Guide
- CLAUDE.md - Repository overview