Your DMARC aggregate reports show a thin but persistent stream of dkim=fail, and every few thousand messages a dkim=temperror. The signature is correct. The private key is fine. The selector matches. What actually happened is that your 2048-bit public key got pasted into a DNS panel that split it across two TXT strings wrong — or worse, someone "hardened" it to 4096 and the answer no longer fits in a UDP packet, so resolvers behind a broken middlebox fall back to TCP, fail, and report a temporary error that looks random.
Here is the whole thing in one sentence: DKIM key sizing is a DNS record-size and interop problem first, and a cryptography problem a distant second. Nobody is factoring your modulus. Your mail is failing because of a length-prefix byte in RFC 1035.
The three choices, and what they actually cost
The honest comparison is measured in the only unit that matters here — the base64 length of the p= tag you have to publish.
- RSA-1024 — public key ≈ 216 base64 chars. Fits in a single TXT string trivially. But it is the deprecated floor: NIST SP 800-57 retired 1024-bit RSA years ago, and Gmail treats sub-1024 keys as an outright fail and 1024 as weak. Do not deploy it new.
- RSA-2048 — ≈ 392 chars. This is the interoperable default, and it is also the *first* size that crosses the 255-byte single-string boundary. Every verifier on earth understands it. Its only cost is that you must publish it as a multi-string record.
- RSA-4096 — ≈ 736 chars. Always multi-string, always pushes the DNS answer toward the size where TCP fallback matters. The security upside over 2048 is marginal for a key you rotate quarterly; the interop downside is real. Skip it.
- Ed25519 — a 32-byte key, ≈ 44 base64 chars. Tiny record, one clean string, fast signing and verifying. Standardized for DKIM in RFC 8463 (
k=ed25519, algorithmed25519-sha256). The catch: not every verifier in 2026 checks it, so you cannot ship it alone.
The recommendation, up front so you can stop reading if you want: Ed25519 plus RSA-2048 dual-sign for anything greenfield; RSA-2048 alone if your tooling can't dual-sign. RSA-4096 is a trap.
Why 255 bytes is the number that rules everything
A DNS TXT record is not a single string. It is a sequence of *character-strings*, and each one is length-prefixed by a single byte. One byte maxes out at 255. So no individual character-string in a TXT record can exceed 255 bytes — a hard structural limit from RFC 1035, not a policy you can raise.
An RSA-2048 public key in p= is ~392 characters. It physically cannot live in one character-string. It must be published as two (or more) quoted strings that the resolver concatenates back together with no separator:
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...255 chars" "...remaining base64 of the key..."The two quoted strings glue edge to edge. There is no space, no comma, no newline between them — the resolver joins the raw bytes. Get that wrong (a stray space inside the quotes, a + mangled into a space, a UI that only saves the first string) and the reconstructed key is garbage, and verification fails with dkim=fail.
That is cliff one. Cliff two is on the wire. Classic DNS over UDP caps the payload at 512 bytes. EDNS0 raises that, but a large TXT answer — a 4096-bit key, or a 2048 key sharing the name with SPF and other TXT records — can push the response past what the path allows, forcing a TCP fallback. Plenty of corporate resolvers, captive portals, and aging middleboxes silently drop DNS over TCP. The verifier can't fetch the key, and instead of a clean fail it emits dkim=temperror — a *temporary* error. Your monitoring sees an intermittent, un-reproducible failure that correlates with nothing. That is the 4096-bit key you deployed "to be safe."
Reading a real DKIM TXT record
Strip a DKIM record down and only a few tags matter:
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA<base64>v=DKIM1— version, always first.k=rsa— key type. Omit `k=` and it defaults to `rsa`. For Ed25519 you must writek=ed25519explicitly.p=— the base64 public key. An empty value (p=) means the key is revoked; verifiers treat it as a hard fail on purpose.t=y— testing mode; verifiers are told not to treat a failure as meaningful. Useful while you validate, but remove it once the key works, or you have effectively disabled enforcement.t=slocks the signature to the exact domain (no subdomaini=).
The Ed25519 equivalent is almost boringly small — one string, done:
v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=Generating and publishing the keys
For a hand-rolled RSA-2048 key, get the exact p= value like this — DER-encode the public key, base64 it, no line wraps:
openssl genrsa -out dkim.private 2048
openssl rsa -in dkim.private -pubout -outform der 2>/dev/null \
| openssl base64 -AThat base64 blob is your p=. Paste it into the record and split at 255 if your DNS provider doesn't do it for you.
With OpenDKIM the plumbing is done for you. Note the date-based selector — 2026a, 2026b — which makes rotation legible at a glance:
opendkim-genkey -b 2048 -s 2026a -d evilmail.pro \
-D /etc/opendkim/keys/evilmail.pro/
chown opendkim:opendkim /etc/opendkim/keys/evilmail.pro/2026a.privateThis drops 2026a.private (the signing key) and 2026a.txt (the ready-to-publish TXT record, already split into quoted strings) in /etc/opendkim/keys/evilmail.pro/. Copy the contents of the .txt file into DNS verbatim.
For Ed25519:
openssl genpkey -algorithm ed25519 -out dkim-ed25519.private
openssl pkey -in dkim-ed25519.private -pubout -outform der 2>/dev/null \
| tail -c 32 | openssl base64 -ADual-signing means running two selectors on outbound mail — say 2026a (k=rsa) and 2026a-ed (k=ed25519) — and publishing both records. In OpenDKIM you list both in the KeyTable/SigningTable; rspamd's dkim_signing module takes a selector_map or per-domain selectors array with one entry per key type. Every verifier picks the strongest algorithm it understands: RFC 8463-aware ones use Ed25519, everyone else uses RSA. You lose nothing and gain forward coverage.
Verifying before you trust it
Never trust a freshly published record. Query it and reassemble:
dig +short TXT 2026a._domainkey.evilmail.prodig prints the record back as separate "..." quoted chunks — concatenate them (drop the quotes and the space between) to see the real key. Then let the signer sanity-check itself against live DNS:
opendkim-testkey -d evilmail.pro -s 2026a -vvvYou want key OK. A key not secure note just means no DNSSEC and is fine.
The ground truth is a live probe. Send to a Gmail or Outlook address, open the raw message, and read Authentication-Results:
Authentication-Results: mx.google.com;
dkim=pass [email protected] header.a=ed25519-sha256;
dkim=pass [email protected] header.a=rsa-sha256Two dkim=pass lines carrying header.a=ed25519-sha256 and rsa-sha256 prove both signatures verified and confirm exactly which algorithm the receiver actually used. During any change, account for DNS propagation and cached negative answers — give it a TTL cycle before concluding anything.
Rotation, and the mistakes that cause outages
DKIM keys have short useful lives, so rotating every 3 to 6 months is plenty. Rotation, not key length, is your real defense: a 2048-bit key you replace quarterly is safer in practice than a 4096-bit key you set in 2021 and forgot.
The overlap window is the part people get wrong:
- 1.Generate a new selector (
2026b) and publish its TXT record. - 2.Switch signing to
2026b. - 3.Keep `2026a`'s record live until all in-flight and queued mail signed with it has been delivered and verified — usually a day or two.
- 4.Only then remove the
2026arecord.
Yank the old record too early and mail still sitting in a receiver's queue fails DKIM on delivery. And never reuse a selector name — once retired, a selector stays dead, because you can't be sure old signed mail isn't still floating around referencing the old key. To hard-revoke a compromised key, publish it with an empty p= rather than deleting the record, so verifiers get an explicit revocation instead of a lookup miss.
Checklist
- Use RSA-2048 as your minimum and default. Never RSA-1024 for new keys.
- Add an Ed25519 dual-sign selector wherever you can — it costs nothing and future-proofs you.
- Keep every TXT character-string ≤ 255 bytes and verify the concatenation reassembles the exact key.
- Confirm DNS over TCP works end-to-end before publishing any oversized answer.
- Use date-based selectors (
2026a,2026b) so rotation state is obvious. - Rotate every 3–6 months with an overlap window; never reuse a selector.
- Validate with
dig+opendkim-testkey+ a live probe readingAuthentication-Results.
Operator's rule of thumb: if your key doesn't fit in a single 255-byte string, the risk you just took on lives in DNS, not in the math — size for the resolver, sign twice, and rotate on a calendar.


