For anyone else with the same problem:
I ended up capitulating and putting my blog behind Cloudflare, but even with bot fight mode activated, it didn’t help. However, a quick look in the actual logs showed that it was a very basic script kiddie attack, and even though the IPs seemed to change (mostly US and UK so far), they hadn’t bothered to spoof the user agent:
"user-agent":"Python/3.12 aiohttp/3.9.5"
So I added a Cloudflare Web Application Firewall (WAF) custom rule to block requests to /api/send-magic-link
when the user agent “contains” Python
and aiohttp
.
I’m still looking for a better solution, that I can implement at the Ghost or Fly.io level, without relying on Cloudflare.