AWS Builder Center
Why DKIM key rotation alone doesn't stop replay attacks

Why DKIM key rotation alone doesn't stop replay attacks

An AWS Security Blog post explains why DKIM replays survive key rotation and p=reject. I rebuilt the attack in a browser simulator to check each claim.

Cloud Engineer
On November 2, 2026, the AWS Security Blog published "Beyond DKIM key rotation: understanding email replay attacks" by Michael Araba. The scenario it opens with is uncomfortable: your DMARC reports show mail from unknown IP addresses passing DKIM as your domain, you rotate the DKIM key, you confirm p=reject is in place, and days later the same traffic still authenticates as you.
I wanted to see that happen rather than just read about it. So I built a small browser simulator with Claude Code, where you can sign an email, let an attacker capture and resend it, and flip each countermeasure on and off. This article covers what the AWS post says, what the simulator does, and what the results showed.

How a replay gets through

DKIM proves one thing: the signed parts of a message did not change between the signer and the receiver. The signature keeps validating wherever the message travels, which is why mailing lists and forwarding work.
A replay uses exactly that.
  • The attacker gets one legitimately signed copy, for example from a mailing list, a compromised mailbox, or a forwarding rule
  • They resend it, unchanged, from their own servers to thousands of new recipients
  • Receivers fetch your public key from DNS, and DKIM passes because nothing changed
  • SPF fails, but DMARC only needs SPF or DKIM to pass with alignment, so DMARC passes too
The attacker cannot edit the message or add content. What they get is your domain's reputation, attached to a message you really sent. The post calls this abuse of reputation and reach, not content forgery.

Why p=reject does nothing here

p=reject tells receivers to reject mail that fails DMARC. During a replay, DMARC passes, so p=reject never applies. It is still the right setting for ordinary spoofing, where the forger has no valid signature and DKIM fails.

Telling replay, spoofing, and relays apart

The post's example DMARC aggregate (rua) report row shows source IP 203.0.113.45, a count of 4,200, SPF fail, and DKIM pass for d=yourdomain.com. A row like that is a signal, not proof.
The post lists three explanations for DKIM-passing mail from an unfamiliar IP.
  • A legitimate relay you haven't inventoried, such as a mailing list or a security gateway
  • A replay, usually with volume well above what you send and to domains you never contacted
  • Ordinary spoofing, which would show DKIM fail instead
To confirm a replay, you need message-level evidence: a message you sent, matched to a copy that reached recipients you didn't address.

Rotation and removal are two different steps

This is the part the post says most rotation guidance leaves out. Rotation means new mail is signed with a new key and selector. Removal means deleting the old public key from DNS so that old signatures, including the captured copy, stop validating. If the old key stays published, every message it ever signed stays replayable.
With Amazon SES, what you can do depends on the DKIM method.
  • With BYODKIM, you publish the key as a TXT record yourself and can delete an old one whenever you need to
  • With Easy DKIM, SES manages the keys through CNAME records, and there is no on-demand way to delete a specific key
For Easy DKIM senders, the post points to early detection, duplicate-message throttling at receivers, and longer term, DKIM2 at the IETF.

Detection with Amazon SES

The post recommends detection before any DNS change. I drew the setup as an architecture diagram (attached separately).
  • Publish SES events (Send, Delivery, Bounce, Complaint) to Amazon Data Firehose or Amazon EventBridge
  • Receive rua reports and read them with a DMARC analyzer
  • Compare the two: DKIM-passing volume from IPs outside your SPF record that exceeds what SES delivered is the signal
  • Log Amazon Route 53 DNS queries to Amazon CloudWatch Logs and alarm on lookups of a selector you rotated away from
You can wire that alarm to an AWS Lambda function that deletes the old selector's record. The post recommends scoping its role to one hosted zone and record, gating it behind confirmed-replay criteria, and says that for most teams, alarm-to-human is safer.




The simulator I built

The simulator is a single HTML file that runs in the browser. It has four panels.
  • Sender: signs a receipt email as example.com and sends it through SES
  • Attacker: captures one copy, can edit the subject or body, and resends it to any number of recipients
  • DNS and keys: DMARC policy, SPF ~all or -all, BYODKIM or Easy DKIM, key rotation, and deleting old keys
  • Receiving server: shows SPF, DKIM, and DMARC results with the reason, and the final action
Below them are a rua-style aggregate table and an event log. Six checklist items tick themselves off as you reproduce each claim from the post. The page also switches between Japanese and English.
Signing is real. The page uses the browser's Web Crypto API to generate 2048-bit RSA keys and sign with RSA-SHA256, so "change one character and the signature fails" actually happens. The DKIM part is simplified: partial relaxed canonicalization and a fixed set of signed headers (from, to, subject, date). It is a learning tool, and the page says so.




What happened when I ran it

I clicked through each scenario and recorded the receiver's verdict.
  • A replay with DMARC at p=reject: SPF softfail, DKIM pass, DMARC pass, delivered
  • A replay after rotating the key, with the old key still in DNS: DKIM pass, delivered
  • A replay after deleting the old key: DKIM fail (no key found), DMARC fail, rejected
  • A spoof with a forged signature at p=reject: DKIM fail, rejected
Editing the captured message also failed, with different reasons. Changing the body broke the body hash, and changing the subject broke the signature itself.
The rua table ended up with a row of the same shape as the example in the AWS post: source IP 203.0.113.45, a count of 4,300, SPF softfail, DKIM pass, and a disposition of delivered.

A mistake that showed why removal timing matters

My first version had a bug. After deleting the old key, an in-flight legitimate email signed with that key still got delivered, because SPF passed for the SES sending IP.
That was wrong for SES. By default, SES uses a subdomain of amazonses.com as the envelope sender, so SPF passes for that domain but does not align with the From domain. For DMARC, DKIM is what carries legitimate SES mail. (A custom MAIL FROM domain changes this, and the simulator assumes you haven't set one.)
After fixing the alignment, deleting the old key too early rejected the in-flight legitimate email at p=reject. That is the reason the AWS post says to leave the old key published until in-flight mail has arrived, and only then remove it. In the simulator, you can now watch both outcomes: delete too early and your own mail fails; wait, and only the attacker's copies fail.
The simulator doesn't touch real mail servers or DNS. Before deleting a key on a real domain, the post's order still applies: confirm the replay at the message level first, get DMARC to enforcement, and then remove the old key once in-flight mail has cleared.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article