Key Rotation Guide¶
This page tracks two distinct key lifecycles:
- Disclosure key — the GPG key reporters use to encrypt vulnerability reports to the maintainer. Single key, public, rotated annually or on compromise. Source of truth for the active fingerprint is
.github/SECURITY.md. - Secrets encryption keys — the Age and SOPS keys that protect encrypted dotfiles in this repo. Multiple keys, per-machine, rotated annually or on personnel / device change.
Both sections live here so a single audit can confirm the project's posture across encrypted-disclosure-in and encrypted-secrets-at-rest.
Disclosure Key (GPG)¶
Closes the rotation half of #870.
Active key¶
| Field | Value |
|---|---|
| Identity | [email protected] |
| Fingerprint | 55AFAD364FD9DB3819E61F0C8D688FAFA9144693 |
| Algorithm | ED25519 signing primary + CV25519 encryption subkey |
| Created | 2026-05-16 |
| Expires | 2029-05-15 (3 years from creation) |
| WKD URL | https://sebastienrousseau.com/.well-known/openpgpkey/hu/qpzqfwauiwxnu1xrf5h47bunsho44p6f |
| Cross-reference | matches the SSH signing key in dot_config/git/allowed_signers.tmpl |
Rotation triggers¶
| Trigger | Response time | Action |
|---|---|---|
| Annual cadence | Within 30 days of expiry | Generate new key, publish, sign transition statement with the old key, archive old key below. |
| Suspected compromise | Same day | Revoke immediately, publish revocation cert to WKD, replay in-flight encrypted disclosures with the new key. |
| Algorithm deprecation | Within 90 days of advisory | Generate replacement on a stronger algorithm; follow annual-cadence steps. |
| Maintainer change | Same day as handover | Old maintainer signs a transition statement; old key revoked 30 days after handover. |
Rotation procedure¶
- Generate the new key (air-gapped where possible):
gpg --quick-generate-key \
"Sebastien Rousseau (Security) <[email protected]>" \
ed25519 sign 1y
- Sign a transition statement with the OLD key:
gpg --clearsign <<EOF
Transition statement, $(date -u +%Y-%m-%dT%H:%M:%SZ).
The disclosure key for [email protected] rotates from:
OLD: <old-fingerprint>
NEW: <new-fingerprint>
Encrypted reports sent to either key during the 30-day overlap
will be accepted. After <UTC-cutoff>, only the new key is honoured.
EOF
- Publish the new key to:
sebastienrousseau.com/.well-known/openpgpkey/hu/(WKD)-
the maintainer's keyoxide profile if applicable
-
Update the "Active key" table above with the new fingerprint and move the previous row into the "Historical disclosure keys" table below with the actual revocation date.
-
Commit, signing with both old and new keys during the overlap:
- After the 30-day overlap, revoke the old key and publish the revocation cert to the same WKD endpoint.
Historical disclosure keys¶
| From | Until | Fingerprint | Reason for rotation |
|---|---|---|---|
| (none yet — first key) | — | — | — |
Age Key Rotation¶
When to Rotate¶
- Annually: As a preventive measure
- After compromise: If you suspect key exposure
- Personnel change: When team members leave
- Device loss: If a device with the key is lost/stolen
Rotation Procedure¶
1. Generate New Key¶
# Create new age key
age-keygen -o ~/.config/chezmoi/key-new.txt
# Note the public key (starts with "age1...")
cat ~/.config/chezmoi/key-new.txt | grep "public key"
2. Re-encrypt All Secrets¶
# List all encrypted files
chezmoi managed --include=encrypted
# For each encrypted file, decrypt with old key and re-encrypt with new
for file in $(chezmoi managed --include=encrypted); do
# Decrypt with old key
chezmoi cat "$file" > "/tmp/$(basename $file).dec"
# Re-encrypt with new key (update chezmoi config first)
# See step 3
done
3. Update Chezmoi Configuration¶
Edit ~/.config/chezmoi/chezmoi.toml:
4. Re-add Encrypted Files¶
# Re-add each secret with new encryption
chezmoi add --encrypt ~/.ssh/id_ed25519
chezmoi add --encrypt ~/.config/secrets/api-keys.env
5. Verify and Clean Up¶
# Verify decryption works
chezmoi cat ~/.ssh/id_ed25519
# Archive old key (store securely offline)
mv ~/.config/chezmoi/key.txt ~/.config/chezmoi/key-$(date +%Y%m%d)-archived.txt
# Rename new key
mv ~/.config/chezmoi/key-new.txt ~/.config/chezmoi/key.txt
# Commit changes
cd ~/.dotfiles
git add -A
git commit -m "chore(secrets): rotate age encryption key"
Emergency Rotation (Compromised Key)¶
# 1. Immediately generate new key
age-keygen -o ~/.config/chezmoi/key-emergency.txt
# 2. Re-encrypt ALL secrets (prioritize most sensitive)
# 3. Revoke/rotate any API keys, tokens that were encrypted
# 4. Update any shared secrets with team members
# 5. Document the incident
SOPS Key Rotation¶
Overview¶
SOPS supports multiple key types. This covers Age keys (recommended) and GPG keys.
SOPS with Age Keys¶
Generate New Key¶
Update .sops.yaml¶
creation_rules:
- path_regex: \.env$
age: >-
age1new_key_here,
age1old_key_here # Keep old key temporarily for decryption
Re-encrypt Files¶
# Rotate key for a single file
sops updatekeys secrets.env
# Or re-encrypt entirely
sops -d secrets.env | sops -e /dev/stdin > secrets.env.new
mv secrets.env.new secrets.env
Remove Old Key¶
After confirming all files are re-encrypted:
- Remove old key from
.sops.yaml - Archive old key securely
- Commit changes
SOPS with GPG Keys¶
# Generate new GPG key
gpg --full-generate-key
# Export fingerprint
gpg --list-keys --keyid-format LONG
# Update .sops.yaml with new fingerprint
# Re-encrypt files
sops updatekeys <file>
Automation Scripts¶
Check Key Age¶
#!/usr/bin/env bash
# check-key-age.sh - Alert if keys are older than 365 days
KEY_FILE="${HOME}/.config/chezmoi/key.txt"
MAX_AGE_DAYS=365
if [[ -f "$KEY_FILE" ]]; then
key_age=$(( ($(date +%s) - $(stat -c %Y "$KEY_FILE")) / 86400 ))
if [[ $key_age -gt $MAX_AGE_DAYS ]]; then
echo "⚠️ Age key is ${key_age} days old. Consider rotation."
exit 1
else
echo "✓ Age key is ${key_age} days old (limit: ${MAX_AGE_DAYS})"
fi
fi
Automated Backup Before Rotation¶
#!/usr/bin/env bash
# backup-before-rotation.sh
BACKUP_DIR="${HOME}/.local/share/dotfiles/key-backups"
mkdir -p "$BACKUP_DIR"
# Backup current key
cp ~/.config/chezmoi/key.txt "$BACKUP_DIR/key-$(date +%Y%m%d_%H%M%S).txt"
# Encrypt backup with passphrase
gpg --symmetric --cipher-algo AES256 "$BACKUP_DIR/key-$(date +%Y%m%d_%H%M%S).txt"
# Remove unencrypted backup
rm "$BACKUP_DIR/key-$(date +%Y%m%d_%H%M%S).txt"
echo "Key backed up to $BACKUP_DIR (encrypted)"
Best Practices¶
- Never commit unencrypted keys - Use
.gitignoreproperly - Store backups offline - USB drive in secure location
- Use hardware keys when possible - YubiKey with age-plugin-yubikey
- Document rotation dates - Keep a log of when keys were rotated
- Test decryption - Always verify after rotation
- Notify team members - If using shared secrets
Recovery Procedures¶
If you lose access to your key:
- Check backups - Offline storage, password manager
- Check other machines - Key may exist on another device
- Re-create secrets - As last resort, regenerate API keys, tokens, etc.
Related Documentation¶
- SECRETS.md - Secrets management overview
- SECURITY.md - Security hardening guide
- Age documentation
- SOPS documentation