The last few weeks have been whack-a-mole with my Mailgun account. My Mailgun account kept getting locked. I would clear a compliance issue, watch everything come back online, and a few hours later it would be frozen again. I could not add a sending domain. I could not remove one. The whole account just sat there, suspended, which halted my ability to spin up email zones for new customers.
Mailgun named issues with three of my domains which were exposed to attackers. By the time I finished pulling server logs, I had counted eighty-six over the last month and half.
Here is the whole thing, start to finish.
It started with a compliance email from Mailgun.
A compliance specialist at Mailgun reached out about recent activity on my account. Two of my client sending domains had been suspended. The reason was suspicious activity. Connection tests hitting my postmaster addresses from 94.26.106.248, an IP registered to a host in Germany. Mailgun told me they had seen that same IP on other compromised accounts running phishing campaigns.
They named a likely cause in the same email. A vulnerability in Gravity SMTP for WordPress, CVE-2026-4020. They had seen it present on similarly affected accounts.
One unauthenticated endpoint exposed credentials.
I route client mail through Gravity SMTP into Mailgun. Each site gets its own Mailgun sending subdomain and its own uniquely generated API key and SMTP password. That isolation is normally a strength. When one site has a problem, it does not touch the others.
The credentials live in the site so the plugin can send. CVE-2026-4020 is a way to read them back out. The tests/mock-data endpoint was meant to power a settings preview inside wp-admin. On vulnerable versions it answered anyone, logged in or not, and it returned the full configuration. The Mailgun SMTP username and password were sitting right there in the response.
A single one log to confirm my theory.
My first instinct was to grep the live server logs for that German IP. Nothing. Not a single request on either suspended site. For a few minutes I believed the attacker had never touched my infrastructure.
Then I remembered the live server only keeps a few days of logs. The connection tests were two weeks back. I pull every rotated log into long-term cold storage, so I went to the archive instead. There it was.
customer-1-fake.com 94.26.106.248 [16/May/2026:12:41:03 +0000] GET
"/wp-json/gravitysmtp/v1/tests/mock-data?page=gravitysmtp-settings"
HTTP/2.0 200 ... 24599 bytes
Three requests in seven minutes on May 16. The first got the http to https redirect. The next two came back 200 with a 24 KB body. That body is the System Report. That is the moment the keys left the website. The second flagged site got hit the same way six days later, on May 22.
Mailgun only sees the keys that get used.
That should have been the end of it. Two sites, confirmed, both rotated. Mailgun does not flag you for leaking a key. It flags you when a key gets used, when something connects or sends with credentials it should not have. The two sites it named were the two where the stolen keys had already been tested.
So the real question was not how did these two leak. It was how many keys got read that nobody had gotten around to using yet. I had a list of every site that was running an outdated Gravity SMTP at the time, the ones my fleet update had to bring current. I took that list and searched every one of their archived access logs for the same endpoint.
It was not three sites. It was eighty-six.
I scanned 8,154 archived access logs across 185 sites. The result were way larger then I expected. This was not someone targeting me. It was a mass scanning campaign, sweeping the internet for this exact endpoint, and my outdated sites answered.
Eighty-six sites returned the full System Report to an unauthenticated request, each one handing over its Mailgun SMTP username and password. The scanning ran from April 30 to June 4. It came from 806 different IP addresses, more than a hundred of which hit several of my sites each. The single IP Mailgun named, 94.26.106.248, was the tenth busiest. It was one bot in a swarm, not the attacker.
Mailgun saw three of the eighty-six. The other eighty-three leaked their credentials and simply had not been abused yet before I rotated the keys out. Rotating all keys was the correct recommendations from Mailgun.
They could read the keys. They could not get in.
The good news is that attackers only got email keys, not anything else. Every request in the server logs was a GET against one read-only endpoint. No POST. Nothing that writes. And the thing that leaked was a Mailgun SMTP credential, not a WordPress password. You cannot log into wp-admin with an SMTP key.
I checked the sites directly to be sure. No new admin accounts. No hidden admins. No logins from any of the scanning IPs. The damage was eighty-six stolen sending credentials and zero site compromises. The blast radius stopped at the mail provider.
The only domain anyone actually sent from was a customer I no longer had.
One of the three Mailgun flagged was different from the rest. The other two blocked connection tests. This one was flagged for sending spam, from a second IP I had not seen, with the subject line “[Mailgun] Gravity SMTP Test Email.”
When I went to investigate it, the site would not let me in. It was a former customer. They had left months ago. The website was gone. But the Mailgun sending zone was still there, still active, still holding a valid credential. So out of eighty-six leaks, the one place email actually went out the door was a dead account I had never cleaned up.
My own audit flagged it. The scanners got there first.
I run a security code audits across my whole fleet. Every plugin and theme gets hash-audited and the findings get logged. After I understood the attack, I looked up Gravity SMTP in my own data.
Sure enough, it was right there. On April 23 my fleet audit had flagged CVE-2026-4020 by name. It described the exact endpoint. Unauthenticated System Report exposure via the mock-data route, returns the full plugin config, affects every version up through 2.1.4. Scored High. Confirmed in source. I had filed it and moved on.
As for timing, I did not catch this ahead of anyone. By April 23 the scanners had already been hitting my sites for at least a week. The earliest probe in my logs is April 17, and my archive only goes back to April 15, so it likely started sooner. One of my sites had already leaked on April 19, four days before my own audit named the bug. My audit did not warn me ahead of the attackers. It documented a door they were already walking through.
When the suspensions hit, 1,543 sites were running Gravity SMTP. 1,351 of them, just under 88 percent, were already on the patched 2.2.0 build. Their licenses had auto-updated them on their own, weeks earlier. The vulnerability was real and my audit caught it and the large majority of customers the fix had already shipped and landed without me touching anything.
What was left was a tail of 192 sites that had drifted onto an older release, spread across 30 different builds. On May 28 I ran a single fleet update with drift steer and brought 187 of them current in one pass. Every site that leaked was in that drifted tail. That tail is exactly where the scanners found their hits.
A handful did not update. Turned out not to need updating at all. When I dug into each one, they were old, unconfigured copies of Gravity SMTP. Leftovers sitting in multi-tenant content directories, a plugin folder still inside a backup, a stale build in an old deploy release. Not one of them had a mailer configured, and not one had a Mailgun zone, so there was nothing in them to leak. The right fix was not to update them. It was to delete them. So I did.
The full timeline, once I had the logs.
So I rotated everything.
Every API key. Every domain sending key. Every SMTP password. With well over a thousand zones, each one carrying a uniquely generated credential per subdomain, doing this by hand would have taken weeks of copy and paste. Generate a key in Mailgun, paste it into the site Gravity SMTP connector, send a test, confirm it landed, move to the next one. A thousand times over.
I did not yet know it was eighty-six sites when I started. I just knew that three named domains felt like the tip of something, and that the only safe assumption was that every key on the account might be compromised. The log audit later proved that instinct right. I was not going to do that by hand.
I leaned on Claude Code and a CSV file.
I drove the rotation in waves. Pull a batch of zones, generate fresh credentials, deploy each one to its site, send a test message, confirm delivery, and log the result. When I lost track of where I was between sessions, I used command-center to search back through previous rotation runs and pick up exactly where I had left off.
The whole run was tracked in a single CSV, mailgun-zones.csv. One row per zone. Mailgun state, matched site, email method, whether the SMTP password and API key had been rotated, and a note for anything unusual.
A 1,469 row CSV is unreadable in a terminal.
So I had Claude Code build me a window into it. One self-contained PHP file that reads mailgun-zones.csv and renders it as a dashboard. Progress bar across the top, a row of counters, and a searchable, filterable table underneath. Open it in the browser and the state of the whole rotation is right there.

The CSV was the source of truth. The dashboard was a throwaway view generated on top of it in a few minutes. I did not write any of that PHP myself. I described what I wanted to see and it appeared. When the numbers changed, I regenerated the page and watched the progress bar climb.
Where it landed.
Of the active zones that actually send mail, 1,289 of 1,349 are rotated. That is 95.6 percent on fresh credentials. The 133 zones flagged for deletion are former customers who have moved on. They were carrying live keys for no reason. One of them is how this whole thing turned from a leak into an actual spam send.
I hope this will be a one time thing however I wouldn’t be surprised if similar bulk rotations are required.
I wish this were a one time event. I do not think it will be. My guess is that bulk key rotations become a normal part of the next year, and not just for me and not just for Mailgun. The same is true for any product or service you hand a credential to. The takeaway I am sitting with is that all of us need to be ready to rotate keys at the first sign of a potential problem. Not after a breach is confirmed. At the first hint.
This could have been so much worse. Eighty-six sites leaked, but only a few test messages ever went out before the keys were rotated dead. If the scanners had moved from harvesting to sending in bulk, it would have torched my sending reputation, and the real customer email would have stopped landing too. The receipts, the password resets, the order confirmations. All of it caught in the blast radius. A poisoned reputation across a thousand domains is a different kind of problem than a few test emails.
The audit found the hole. Auto-updates closed it for most. drift steer closed it for the rest. And rotating every key, not just the three Mailgun named, is the only reason eighty-three other stolen credentials are now worthless instead of live. The detection was never the hard part. The distance between a fix existing and a fix running on every site is the whole game.
The IPs, if you are fighting this too.
If you run Gravity SMTP, or you host for other people who do, this campaign is still active and you are probably seeing it. Search your access logs for any request to gravitysmtp/v1/tests/mock-data. A 200 response with a body around 22 to 24 KB means that site handed over its credentials and you need to rotate them now. A 401 or a 404 means you were already patched.
These are the fifteen busiest scanning IPs I saw, ranked by confirmed credential leaks against my own sites.
| IP address | Requests | Sites hit | Confirmed leaks |
|---|---|---|---|
45.148.10.120 | 1,087 | 171 | 536 |
194.26.192.219 | 441 | 69 | 437 |
195.178.110.31 | 835 | 113 | 233 |
37.166.41.10 | 172 | 37 | 171 |
193.32.162.60 | 567 | 107 | 134 |
213.209.159.229 | 289 | 72 | 104 |
37.167.141.57 | 88 | 31 | 88 |
93.123.109.214 | 232 | 98 | 83 |
139.59.220.152 | 90 | 18 | 77 |
94.26.106.248 | 262 | 11 | 76 |
45.148.10.62 | 223 | 103 | 74 |
23.180.120.134 | 228 | 57 | 67 |
78.242.187.26 | 67 | 28 | 66 |
185.177.72.60 | 198 | 80 | 61 |
37.166.253.135 | 55 | 30 | 53 |