Rotate signing keys
Distinct from rotate-secrets.md because signing keys have
external verifiers (subscribers, browsers, recipients) that need
to keep validating old artifacts during the rollover window.
Webhook HMAC secrets
Per-webhook, not global. Each webhook has its own secret. The owner rotates by editing the webhook in Settings → Webhooks → the hook → "Rotate secret" → set new value.
- Subscriber MUST be updated to the new secret first (or accept both during the cutover).
- The previous secret is not kept; immediate rotation cuts off old-signature validation.
- For ops-driven mass rotation (suspected key store compromise):
shithubd webhook rotate-allregenerates every secret. All subscribers will start gettingsignature mismatchuntil the owners update their endpoints.
Notification unsubscribe HMAC
The one-click unsubscribe links in emails are HMAC-signed. The
key lives in notif.unsubscribe_key. Rotation invalidates every
old email's unsubscribe link (a recipient with an unread
email containing a link from before rotation will get "invalid
link" if they click it after rotation).
Procedure:
- Generate a new key:
openssl rand -base64 32. - Replace
notif.unsubscribe_keyinworker.env. - Redeploy worker (
ANSIBLE_TAGS=app). - Optional: add a banner to the in-app inbox letting users know unsubscribe links from old emails will not work.
Cursor signing key
internal/pagination/keyset HMACs every cursor we hand out.
Rotation invalidates every cursor in flight. The user-visible
effect is: any open browser tab with a "Next page" link from
before the rotation will return "invalid cursor" → take them
back to page 1.
Procedure mirrors the unsubscribe key. Acceptable to do without warning; the worst case is a user clicks "Next" and gets sent back to the top of the list.
TLS certificates (Caddy-managed)
Caddy obtains and renews certs from Let's Encrypt automatically. Manual rotation is rarely needed; if it is:
ssh edge
sudo caddy reload --config /etc/caddy/Caddyfile
If Let's Encrypt is rate-limiting you, switch the
caddy_use_acme_staging inventory flag to true, redeploy,
verify, then flip back. Production cert reissue is constrained
by LE's per-week limits; mass rotation in a single hour will
fail.
SSH host keys
The host keys (/etc/ssh/ssh_host_*) are sshd's identity to
clients. Rotating them prompts every git client with "WARNING:
REMOTE HOST IDENTIFICATION HAS CHANGED!" — disruptive.
Only rotate if the host has been compromised. Procedure:
ssh-keygen -Aon the host (regenerates all host keys).systemctl restart sshd.- Publish the new host-key fingerprints somewhere users can verify (status page, security advisory).
- Users prune the old line from
~/.ssh/known_hostsand re-accept on first reconnect.
User-facing communication is critical here. Without it, you'll look like a MITM attacker to every developer's SSH client.
View source
| 1 | # Rotate signing keys |
| 2 | |
| 3 | Distinct from `rotate-secrets.md` because signing keys have |
| 4 | external verifiers (subscribers, browsers, recipients) that need |
| 5 | to keep validating old artifacts during the rollover window. |
| 6 | |
| 7 | ## Webhook HMAC secrets |
| 8 | |
| 9 | Per-webhook, not global. Each webhook has its own secret. The |
| 10 | owner rotates by editing the webhook in Settings → Webhooks → |
| 11 | the hook → "Rotate secret" → set new value. |
| 12 | |
| 13 | - Subscriber MUST be updated to the new secret first (or accept |
| 14 | both during the cutover). |
| 15 | - The previous secret is **not** kept; immediate rotation cuts |
| 16 | off old-signature validation. |
| 17 | - For ops-driven mass rotation (suspected key store compromise): |
| 18 | `shithubd webhook rotate-all` regenerates every secret. All |
| 19 | subscribers will start getting `signature mismatch` until the |
| 20 | owners update their endpoints. |
| 21 | |
| 22 | ## Notification unsubscribe HMAC |
| 23 | |
| 24 | The one-click unsubscribe links in emails are HMAC-signed. The |
| 25 | key lives in `notif.unsubscribe_key`. Rotation invalidates every |
| 26 | **old email's** unsubscribe link (a recipient with an unread |
| 27 | email containing a link from before rotation will get "invalid |
| 28 | link" if they click it after rotation). |
| 29 | |
| 30 | Procedure: |
| 31 | |
| 32 | 1. Generate a new key: `openssl rand -base64 32`. |
| 33 | 2. Replace `notif.unsubscribe_key` in `worker.env`. |
| 34 | 3. Redeploy worker (`ANSIBLE_TAGS=app`). |
| 35 | 4. Optional: add a banner to the in-app inbox letting users |
| 36 | know unsubscribe links from old emails will not work. |
| 37 | |
| 38 | ## Cursor signing key |
| 39 | |
| 40 | `internal/pagination/keyset` HMACs every cursor we hand out. |
| 41 | Rotation invalidates every cursor in flight. The user-visible |
| 42 | effect is: any open browser tab with a "Next page" link from |
| 43 | before the rotation will return "invalid cursor" → take them |
| 44 | back to page 1. |
| 45 | |
| 46 | Procedure mirrors the unsubscribe key. Acceptable to do without |
| 47 | warning; the worst case is a user clicks "Next" and gets sent |
| 48 | back to the top of the list. |
| 49 | |
| 50 | ## TLS certificates (Caddy-managed) |
| 51 | |
| 52 | Caddy obtains and renews certs from Let's Encrypt automatically. |
| 53 | Manual rotation is rarely needed; if it is: |
| 54 | |
| 55 | ```sh |
| 56 | ssh edge |
| 57 | sudo caddy reload --config /etc/caddy/Caddyfile |
| 58 | ``` |
| 59 | |
| 60 | If Let's Encrypt is rate-limiting you, switch the |
| 61 | `caddy_use_acme_staging` inventory flag to `true`, redeploy, |
| 62 | verify, then flip back. Production cert reissue is constrained |
| 63 | by LE's per-week limits; mass rotation in a single hour will |
| 64 | fail. |
| 65 | |
| 66 | ## SSH host keys |
| 67 | |
| 68 | The host keys (`/etc/ssh/ssh_host_*`) are sshd's identity to |
| 69 | clients. Rotating them prompts every git client with "WARNING: |
| 70 | REMOTE HOST IDENTIFICATION HAS CHANGED!" — disruptive. |
| 71 | |
| 72 | Only rotate if the host has been compromised. Procedure: |
| 73 | |
| 74 | 1. `ssh-keygen -A` on the host (regenerates all host keys). |
| 75 | 2. `systemctl restart sshd`. |
| 76 | 3. Publish the new host-key fingerprints somewhere users can |
| 77 | verify (status page, security advisory). |
| 78 | 4. Users prune the old line from `~/.ssh/known_hosts` and |
| 79 | re-accept on first reconnect. |
| 80 | |
| 81 | User-facing communication is critical here. Without it, you'll |
| 82 | look like a MITM attacker to every developer's SSH client. |