You set up DKIM, published the public key in DNS, watched alignment pass in your DMARC reports, and moved on. That was 18 months ago. It’s the same key today.
That’s normal, and it’s also where DKIM setups quietly rot. A session token expires. An API key has a 30-day TTL. A DKIM private key sits in a mail server config file for years because nothing forces it to change, and “nothing broke” looks identical to “nobody’s compromised it yet.”
What the key is actually protecting
Your mail server signs outgoing messages with a private key. Receiving servers fetch the matching public key from DNS and check the signature. If it verifies, the message passes DKIM. The selector tag (s=) in the DKIM-Signature header points the verifier at which DNS TXT record holds that public key, so you can run several keys at once under different selectors.
The private key is supposed to never leave your mail server. If it does, an attacker can sign messages exactly as your server would. Those messages pass DKIM. They pass DMARC. Spam filters treat them as legitimate, because cryptographically they are. A fake invoice or a credential-theft email signed with your own leaked key looks identical to your real outgoing mail.
You won’t see this in your DMARC reports either: a forged message signed with your real key produces a clean DKIM pass, same as a legitimate one. The only signal worth watching is unusual sending volume from IPs you don’t recognize. If you’re not already reading your aggregate reports for that, see reading DMARC aggregate reports without drowning in XML.
Where private keys actually leak
Rarely in a dramatic breach. Usually from:
- Config backups and repos. A mail server’s
main.cfor OpenDKIMKeyTablesometimes ends up committed to a repo or dropped in an S3 bucket. Anyone with read access to that repo or bucket has the key. - VM snapshots and exports. Snapshotting a server for migration or spinning up a staging box from a production image copies whatever’s in the keystore along with it.
- Stale access. Whoever has shell access to the mail server can read the key file. Access doesn’t always get revoked the day someone leaves.
- Managed sending providers. If SendGrid, SES, or another provider signs on your behalf, a security incident on their side can expose your signing capability without touching your own servers at all.
None of that needs a sophisticated attacker. Most of it needs one honest mistake.
What RFC 6376 actually says about rotation
It doesn’t hand you a rotation schedule. There’s no “rotate every 12 months” requirement in the spec. What it does specify is how to do it without breaking mail: when rotating to a new key pair, signing should switch to the new private key immediately, and the old public key should stay published for a reasonable validation interval before you remove it, because mail already in transit was signed with the old key and a verifier might not check it right away.
That overlap window is what makes rotation safe. There’s no point where a message gets signed with a key nobody can verify yet.
Step-by-step
1. Generate a new key pair.
openssl genrsa -out dkim-private-2026.key 2048
openssl rsa -in dkim-private-2026.key -pubout -out dkim-public-2026.key
2. Publish the new public key under a new selector, leaving the old one in place:
dkim2026._domainkey.example.com TXT "v=DKIM1; k=rsa; p=<new-base64-public-key>"
3. Wait out DNS propagation. DKIM TXT records commonly ship with a 3600-second TTL, so give it at least an hour before moving on.
4. Switch the mail server to sign with the new selector. For Postfix + OpenDKIM, that’s the Selector and KeyFile entries in your signing table. For a managed sender, it’s a setting in their dashboard.
5. Confirm it, then optionally test it quietly first. RFC 6376’s t=y flag on the new key record tells verifiers “this domain is testing DKIM”; they MUST NOT treat a failed signature under a testing key any differently from unsigned mail. Publish the new selector with t=y, confirm signatures verify and DMARC passes, then drop the flag once you trust it. Check with:
dig TXT dkim2026._domainkey.example.com +short
6. Retire the old selector. After the overlap window, either remove the TXT record or publish it with an empty p=. RFC 6376 §3.6.1 defines an empty p= as a revoked key, and §3.1 says there’s no defined difference between a revoked and a removed key: both make the signature fail. Revoking just states on the record that you meant to retire it.
Key length and algorithm
A DKIM key set up a few years ago is often 1024-bit RSA, which was the norm then. RFC 8301 requires at least 1024-bit RSA and recommends 2048-bit, so treat 2048 as your target and 1024 as the floor. The alternative is Ed25519, which uses a much shorter key (44 base64 characters) and so a much shorter TXT record. Ed25519 isn’t accepted everywhere yet: some older receivers and a few managed platforms only handle RSA. 2048-bit RSA stays the safe default; use rotation as the moment to upgrade if you’re still on 1024-bit.
Make it a schedule, not a one-off
Rotation fails as an ad-hoc task. The overhead of remembering how to do it safely never gets prioritized until something forces it. Treat it like certificate renewal: pick an interval (12 months is reasonable for most teams, 6 months for anything handling financial or health data, or right after staff with server access leave), put a reminder 90 days ahead of the date, and follow the same six steps every time.
If your DMARC policy is still at p=none while you sort this out, see moving from p=none to p=reject safely for the rollout order once your senders are clean.
DMARC Guardian’s DNS records page shows exactly which DKIM, SPF, and DMARC records are currently published for a domain, checked hourly, so you can confirm the new selector actually replaced the old one instead of guessing from dig output alone.