Setting expiry and renewal reminders for signing keys Jump to heading
Key expiry is a safety net: a key that leaks and is never noticed stops being trusted on a known date. It is also, reliably, the cause of a confused message on the morning of the anniversary, when a contributor’s commits suddenly fail verification and nobody remembers why. The problem is not expiry; it is expiry without a reminder. This page covers choosing sensible periods, encoding them for both GPG and SSH keys, and building three layers of warning — personal, team and CI — so renewal happens a month early rather than an hour late. It belongs to protecting and rotating signing keys.
When to use this approach Jump to heading
- Your team signs commits and has had at least one “my commits stopped verifying” surprise.
- You are setting up signing and want expiry from the start rather than retrofitting it.
- Your trust file or forge has keys with no expiry at all, and you want to introduce one without breaking anyone.
- You maintain a shared allowed signers file and need a view of upcoming expiries across the team.
Step 1 — Choose periods that match each key’s role Jump to heading
Shorter periods limit how long a silent leak stays useful, but each renewal costs effort. Match the period to how hard renewal is and how exposed the key is.
Step 2 — Encode expiry for GPG keys Jump to heading
GPG stores expiry in the key itself. Set it when creating the key, or change it later with the primary available.
# At creation
gpg --quick-add-key "$PRIMARY_FPR" ed25519 sign 1y
# Change later (requires the primary secret key)
gpg --quick-set-expire "$PRIMARY_FPR" 1y "$SUBKEY_FPR"
# Read expiry dates in a script-friendly way
gpg --list-keys --with-colons "$PRIMARY_FPR" |
awk -F: '$1=="sub" || $1=="pub" {print $1, $5, ($7 ? strftime("%F",$7) : "never")}' After changing expiry, re-export and re-upload the public key. Forges and collaborators keep the old expiry until they receive the new public key.
Step 3 — Encode expiry for SSH keys Jump to heading
SSH keys have no built-in expiry. The expiry lives in the trust file instead, as valid-before, and it applies wherever that file is used for verification.
# allowed_signers
[email protected] namespaces="git",valid-after="20251001",valid-before="20261101" ssh-ed25519 AAAA... # List upcoming SSH expiries from the trust file
grep -o '^[^ ]*\|valid-before="[0-9]*"' allowed_signers | paste - - |
sed 's/valid-before="\([0-9]*\)"/\1/' | sort -k2 | head Forges do not read valid-before; a key registered on the forge stays valid there until removed. If forge verification matters, pair the trust-file date with a policy of removing old keys from accounts at renewal.
Step 4 — Generate a team expiry report Jump to heading
A weekly report of keys expiring in the next sixty days catches what personal reminders miss. It reads the same sources your gates use.
#!/bin/sh
# expiring-keys.sh — keys in allowed_signers expiring within N days
set -eu
days=${1:-60}
limit=$(date -d "+$days days" +%Y%m%d)
today=$(date +%Y%m%d)
sed -n 's/^\([^ ]*\) .*valid-before="\([0-9]\{8\}\)".*/\2 \1/p' allowed_signers |
awk -v t="$today" -v l="$limit" '$1>=t && $1<=l' | sort # Weekly job posting the report as an issue comment or chat message
on: { schedule: [{ cron: "0 8 * * 1" }] }
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./expiring-keys.sh 60 | tee report.txt
- run: '[ -s report.txt ] && gh issue comment 42 --body-file report.txt || true'
env: { GH_TOKEN: "${{ github.token }}" } Step 5 — Warn in CI before the key fails Jump to heading
The most effective reminder lands where the owner is already looking. A CI step that checks the signer of the pull request’s commits and warns when their key expires within fourteen days is cheap and hard to miss.
# In the signature check job, after verification passes
signer=$(git log -1 --format='%GS' HEAD)
exp=$(sed -n "s/^$signer .*valid-before=\"\([0-9]\{8\}\)\".*/\1/p" "$SIGNERS_FILE" | sort | tail -1)
if [ -n "$exp" ] && [ "$exp" -le "$(date -d '+14 days' +%Y%m%d)" ]; then
echo "::warning::Signing key for $signer expires on $exp — renew it now"
fi Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Do commits signed before expiry stop verifying afterwards? Jump to heading
For GPG, Git reports an expired key’s signature as Y (good signature, expired key) rather than G, so strict scripts treat it as failing even for old commits. For SSH with valid-before, signatures are checked against the commit’s timestamp, so old commits still verify. Historical audits need to account for the GPG behaviour.
Is no expiry ever acceptable? Jump to heading
For a key on hardware that cannot be extracted, some teams accept no expiry and rely on revocation. For any key stored as a file, an expiry is cheap insurance against a leak nobody noticed.
Can expiry be extended instead of rotating? Jump to heading
Yes, for GPG, and it is often the right call when nothing suggests compromise. For SSH, “extending” means editing the valid-before date in the trust file. Rotating is better when the key has been in use on a machine you no longer fully trust.
Related Jump to heading
- Protecting & Rotating Signing Keys — the parent topic.
- Troubleshooting ‘gpg failed to sign the data’ — what an unnoticed expiry looks like.
- Keeping a GPG Primary Key Offline — the ceremony needed to extend GPG expiry.
- Building an Allowed Signers File from Team Membership — where SSH expiry dates live.