DKIM keys are like passwords — they should be rotated periodically, especially after any suspected compromise (supplier breach, departed-employee access, leaked private key from a misconfigured backup). The standing recommendation: rotate annually as preventive hygiene, immediately after any incident. The trick is doing it without breaking deliverability for in-flight messages — DKIM signature verification depends on the receiver being able to fetch the public key from DNS at the time the message is processed, which can be hours after sending for big mailbox providers.
This playbook covers the why-when-how, the AcelleMail-side commands, the 14-day overlap window that prevents in-flight verification failures, and the FBL-coupling gotcha (Yahoo + Comcast key the FBL on DKIM signature) that catches every operator on first rotation.
Why rotate#
Three distinct cases:
- Routine rotation — annual or semiannual. Best-practice hygiene; reduces risk if a key was silently compromised. Most operators don't do this; the ones that do it reduce their long-tail incident risk meaningfully.
- Compromise rotation — after any suspected leak. Old AcelleMail backups including unencrypted private keys, an SSH key disclosure that exposed the host filesystem, an
apt install from a tampered repository — these all require rotation.
- Algorithm upgrade — moving from 1024-bit to 2048-bit RSA, or to Ed25519 in the future. AcelleMail's
generateDkimKeys() defaults to 1024-bit RSA (per app/Model/SendingDomain.php); 2048-bit is preferred for new keys.
The 14-day overlap window#
The receiver's view of your DKIM key isn't real-time. Mail providers cache DNS TXT records aggressively (typical TTL: 1 hour, but cached results may live longer in some receivers' resolver chains). A message sent at T=0 may not be DKIM-verified until T=24h or later for greylisted/deferred mail.
The corollary: when you swap selectors, the old selector's TXT record must remain published for as long as any in-flight message could be verified. The safe window is 14 days. The pattern:
T-14d: Generate new keypair, publish new selector to DNS
Old selector remains published, AcelleMail still signing with old
T=0: AcelleMail switches to new selector
Old selector remains published as fallback for in-flight
T+14d: Old selector can be safely removed from DNS
Skipping the overlap leads to brief DKIM-verification failures during the swap — Yahoo and Microsoft will treat unverifiable messages as suspect, costing days of reputation lift.
Step-by-step rotation#
Pre-flight checks#
# 1. Confirm the current selector is published
dig +short TXT acelle1._domainkey.example.com
# 2. Confirm AcelleMail is signing with this selector
cd /var/www/acellemail
php artisan tinker --execute='
$d = \Acelle\Model\SendingDomain::where("name","example.com")->first();
echo "selector: {$d->dkim_selector}\n";
echo "key length: ".strlen($d->dkim_private_key)." chars\n";
'
# 3. Send a test message + verify with mail-tester.com or Glock Apps —
# record current DKIM pass result as baseline
Step 1 — Pre-stage the new selector (T-14d)#
cd /var/www/acellemail
php artisan tinker --execute='
$d = \Acelle\Model\SendingDomain::where("name","example.com")->first();
// Generate new keypair WITHOUT swapping yet
$newSelector = "acelle".date("Y").$d->id; // e.g. "acelle20262"
$keys = $d->generateDkimKeys(); // returns [private, public]
// Stash for later swap — store in a temporary place (a comment, your password manager)
echo "NEW SELECTOR: $newSelector\n";
echo "NEW PUBLIC: ".$keys["public"]."\n";
echo "NEW PRIVATE (save securely): ".$keys["private"]."\n";
'
Publish the new public key to DNS:
TXT acelle20262._domainkey.example.com
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4..."
Wait at least 1 hour for propagation, then verify:
dig +short TXT acelle20262._domainkey.example.com
# Should return the new public key
Step 2 — Wait the 14-day window (T-14d → T=0)#
During this window, AcelleMail keeps signing with the old selector. Both selectors are valid in DNS. In-flight messages can still verify against the old key.
Use this time to:
- Update FBL registrations that key on DKIM (Yahoo, Comcast — see feedback-loops-fbl-setup).
- Verify the new selector's DNS publication is stable.
- Plan the rollback path (see below).
Step 3 — Switch AcelleMail to new selector (T=0)#
cd /var/www/acellemail
php artisan tinker --execute='
$d = \Acelle\Model\SendingDomain::where("name","example.com")->first();
$d->dkim_selector = "acelle20262"; // the new selector from step 1
$d->dkim_private_key = "<new private key>"; // saved from step 1
$d->save();
echo "switched.\n";
'
Send a test message + verify it now signs with the new selector. The DKIM-Signature header should show s=acelle20262;.
Run a smoke test on a representative campaign before the next mass send — confirm pass rates stay 100 % at Postmaster + SNDS over 24 hours.
Step 4 — Wait another 14 days (T+14d)#
Old selector remains in DNS for in-flight verification. Don't remove it yet.
Step 5 — Remove old selector from DNS (T+14d)#
Once 14 days have passed since the switch:
# Remove from DNS:
TXT acelle1._domainkey.example.com → DELETED
After 24 hours of clean sending without the old selector, rotation is complete.
The FBL-coupling gotcha#
Yahoo and Comcast both key their FBL on DKIM signature — when their abuse system generates an ARF report for a complaint, the report only delivers if the original message was signed with a DKIM selector they recognize. Switching selectors silently breaks the FBL until the new selector is registered.
The fix: before switching selectors (step 3), contact each FBL provider:
Both providers usually approve within 24 hours. After approval, ARF reports with the new selector start arriving.
AcelleMail-specific verification#
After step 3, run AcelleMail's domain verifier:
cd /var/www/acellemail
php artisan tinker --execute='
$d = \Acelle\Model\SendingDomain::where("name","example.com")->first();
$result = $d->verify();
echo "DKIM: " . ($result["dkim"] ? "PASS" : "FAIL") . "\n";
echo "SPF: " . ($result["spf"] ? "PASS" : "FAIL") . "\n";
'
The verifyDkim() method on SendingDomain (per app/Model/SendingDomain.php) checks the published key matches the configured key. Both must match — false negative usually means DNS hasn't propagated; wait an hour and retry.
Rollback path#
If something goes wrong post-step-3 (sudden Postmaster authentication drop, FBL stops delivering, etc.), rollback:
php artisan tinker --execute='
$d = \Acelle\Model\SendingDomain::where("name","example.com")->first();
$d->dkim_selector = "acelle1"; // the OLD selector
$d->dkim_private_key = "<old private key>"; // hopefully you saved it
$d->save();
echo "rolled back.\n";
'
Critical: keep the old private key for at least 30 days post-rotation in a secure location. AcelleMail doesn't preserve it; once you swap, the old key is gone from the DB.
Per-customer rotation#
In multi-tenant AcelleMail, each customer has their own SendingDomain rows with their own DKIM keys. Rotate them per customer; running an org-wide rotation script:
php artisan tinker --execute='
foreach (\Acelle\Model\SendingDomain::all() as $d) {
if ($d->dkim_selector_age_days() > 365) {
echo "ROTATE: {$d->name} (selector {$d->dkim_selector}, age " . $d->dkim_selector_age_days() . "d)\n";
}
}
'
(The dkim_selector_age_days() method requires custom code; not stock AcelleMail. The pattern is "find customers with stale keys" → notify them → run rotation per the steps above.)
Related reading#
FAQ#
Can I rotate without the 14-day window?#
Technically yes, but expect 24-48 hours of degraded deliverability. Postmaster authentication graphs will show DKIM drops; receivers will treat verifying-with-cached-key messages as failed; reputation lifts a few percent. The 14-day window is the conservative path; it costs only DNS-record bookkeeping.
What about Ed25519 vs RSA?#
Ed25519 DKIM keys (RFC 8463) are smaller and faster to verify. Acelle's stock generateDkimKeys() uses RSA; Ed25519 requires a code change. Worth it for very-high-volume senders where signing CPU matters; not worth the complexity for most.
What if I missed the 14-day window?#
If you removed the old selector before 14 days post-swap, deliverability hits for 24-48 hours then recovers. Don't try to roll back to the old selector — the new one is already in production traffic; reverting double-rolls the verification.
How do I know if a key was compromised?#
Watch for: unexpected mail from your domain (not sent by you), Postmaster Authentication SPF/DKIM rates dropping when you didn't change anything, receivers reporting forged senders. Any of these → rotate immediately.
Annual rotation calendar#
Set a recurring calendar event for the rotation cycle:
| Month |
Action |
| Jan 1 |
Audit current key age across all sending domains. Schedule rotation for any > 12 months old. |
| Feb 1 |
Pre-stage new selectors (T-14d phase) for the year's rotation cohort. |
| Feb 15 |
Switch to new selectors (T=0 phase). |
| Mar 1 |
Remove old selectors from DNS (T+14d phase). Confirm FBL re-registration with Yahoo + Comcast happened. |
| Mid-year |
Audit SPF + DMARC alignment quarterly to catch drift. |
The calendar discipline is what separates "we should rotate" from "we did rotate this year." Without the calendar reminder, rotation slips to year 2, then year 3, then "we'll do it next time something breaks" — by which time the original team that knew the key has moved on, the private key location is unclear, and the rotation becomes a multi-week incident project instead of a 30-minute task.
Why 1024-bit is no longer enough#
AcelleMail's stock generateDkimKeys() defaults to 1024-bit RSA. For low-volume legacy installs this is acceptable (the cryptographic break of 1024-bit RSA is theoretical for typical attackers), but new installs in 2026 should use 2048-bit. The change requires editing app/Model/SendingDomain.php's key-generation parameters or generating keys externally and pasting them in. 2048-bit DKIM TXT records are slightly larger (some receivers' DNS responses split into multiple TXT segments) — most receivers handle this fine, but very-old MTA implementations may not.
The migration: same 14-day overlap window — generate new 2048-bit keypair, publish, switch, retire. Just a one-time upgrade per sending domain.