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.

Suggested expiry periods by key roleAn offline primary key can live for years because it is rarely exposed. A laptop signing key or subkey is exposed daily and renewal is cheap, so a year is a good balance. CI signing keys are best measured in days or replaced by keyless signing.Suggested expiryRenewal effortoffline GPG primary2–5 yearsceremony, rarelaptop subkey / SSH key12 months5 minuteshardware-held key2–3 yearsre-registerCI signing keydays, or keylessautomatedthe cheaper renewal is, the shorter the period can be

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.

Three reminders before an expirySixty days before expiry the owner receives a personal reminder. Thirty days before, the team report lists the key. Fourteen days before, CI starts printing a warning on the owner's pull requests. Renewal at any of these points avoids the failure on the day.Calendarpersonal reminder−60 daysTeam reportlisted by owner−30 daysCI warningon the owner's PRs−14 daysLast chancerenew or rotate−1 dayExpiryverification fails0three nudges from three directions — nobody ignores all of them

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
Renewing an SSH signing keyThe owner generates a new key, registers it with the forge as a signing key, and opens a change adding it to the trust file with a valid-after date. When that merges, the old key's line gets a valid-before date and the old forge registration is removed after a grace period.Generatessh-keygen -t ed25519Registerforge signing keyTrust filenew line, valid-afterClose oldvalid-before setoverlap the two keys by a few days so nothing in flight fails

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.