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.

1,469
Mailgun zones
1,289
Keys rotated
86
Sites that leaked keys
3
Mailgun actually flagged

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.

CVE-2026-4020
An unauthenticated REST endpoint in Gravity SMTP. A single GET request to gravitysmtp/v1/tests/mock-data returns the plugin System Report, which includes the live SMTP connection settings. No login. No nonce. Anyone who knew the URL could read the keys straight out of the site.

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.

86
Sites that leaked credentials
806
Distinct scanning IPs
36 days
Of continuous scanning
0
WordPress sites breached

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.

What the campaign got, and did not get
Got: the Mailgun SMTP username and password from 86 sites, by reading one unauthenticated endpoint over and over. Did not get: a single WordPress login, admin account, or write to any site. A leaked sending key lets you send mail through Mailgun. It does nothing on the site it came from. Every one of those keys has since been rotated and is now dead.

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.

1,543
Sites on Gravity SMTP
87.6%
Already on the patched build
192
Drifted behind
187
Brought current in one run

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.

Patched upstream is not deployed everywhere
This is the real gap, and it is not unique to Gravity SMTP. A fixed version existing does not mean every site is running it. Detection found the hole. Auto-update closed it for nearly 88 percent of sites on its own. drift steer closed it for the drifted tail. The whole exposure lived in the 12 percent that had fallen behind, which is exactly where an attacker goes looking first.

The full timeline, once I had the logs.

March 25, 2026
Rocketgenius ships Gravity SMTP 2.1.5 with the fix. The exposure is closed in the latest release. This is six days before the CVE is even public.
March 31, 2026
CVE-2026-4020 is published. CVSS 7.5. Unauthenticated System Report exposure via the mock-data endpoint, affecting every version through 2.1.4. The clock for the drifted sites starts here.
April 17, 2026
The earliest probe I can find in my logs. My access-log archive only goes back to April 15, so the scanning had almost certainly started earlier, in the days right after disclosure.
April 19, 2026
First confirmed leak on my fleet. A drifted site returns the full System Report to an unauthenticated GET. Four days before I would flag the CVE myself.
April 23, 2026
My fleet audit flags CVE-2026-4020 by name, scored High. By now the scanners have been harvesting my drifted sites for at least a week. I file it and move on.
May 16 and May 22
The two sites Mailgun would later name leak their credentials to 94.26.106.248. Each returns a 24 KB System Report to an unauthenticated GET.
Late May
A stolen credential from a former customer orphaned zone is used to send a Gravity SMTP test email. The only domain where mail actually went out.
May 28, 2026
Mailgun suspends the flagged domains. The same day I run drift steer and bring the entire drifted tail current to 2.2.0, closing the endpoint.
May 30, 2026
I start rotating every key on the account, not just the three Mailgun named, because by now I suspect the leak is far wider than three sites.
June 2 to June 5
Rotation crosses 95 percent, the orphaned zone is deleted, and the tickets close. The fleet-wide log audit confirms 86 sites leaked. Every key is now fresh.

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.

Claude Code Session
Austin
Rotate the Mailgun SMTP info for mg.customer-1-fake.com.
Claude
Generated a new SMTP password and API key for mg.customer-1-fake.com, deployed them to the site Gravity SMTP connector, sent a test message, and confirmed delivery. Logged the rotation to mailgun-zones.csv.
Austin
Keep going down the list. Skip any zone that has no matching site.
Claude
Rotated 38 zones this wave. 4 had no CaptainCore site match, flagged for review. 11 belong to former customers, flagged to delete. Updated the CSV after each one.

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.

PHP dashboard reading mailgun-zones.csv showing 1,469 zones, 1,289 rotated, 95.6 percent of active zones complete, with counters for to rotate, needs review, to delete, and receive-only

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.

Where the 1,469 zones sorted out
Rotated
1,289
To delete (former customers)
133
Needs review
107
Still to rotate
60
Receive only (no rotation needed)
13

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 addressRequestsSites hitConfirmed leaks
45.148.10.1201,087171536
194.26.192.21944169437
195.178.110.31835113233
37.166.41.1017237171
193.32.162.60567107134
213.209.159.22928972104
37.167.141.57883188
93.123.109.2142329883
139.59.220.152901877
94.26.106.2482621176
45.148.10.6222310374
23.180.120.1342285767
78.242.187.26672866
185.177.72.601988061
37.166.253.135553053
Top 15 scanning IPs by confirmed credential leaks out of 806 unique IPs.