Observations about spam signups

This observation matches with this: Spam for Lead Generation?

Are you able to see the IP addresses of send-magic-link requests? Can you check if they are also from Tor network? Maybe attacker is now checking another mail list.

1 Like

Where could I find the IP addresses?

Ghost itself doesn’t collect access logs. You need to collect access logs on your setup, then filter it by the /members/api/send-magic-link/ path.

I now see that the pattern I mentioned was just the tip of the iceberg on a larger spam attack more similar to the ones I’ve experienced before.

The difference was that this time it was going through the front end, and all the traffic was from exist node IP addresses. So I had to blacklist all exit nodes from that endpoint with my Nginx reverse proxy.

I’m still getting failed spam to the unchanged magic link endpoint, coming from the clearnet, but it just 404s :)

Another thing I did, finally, which I recommend other self-hosters do also, is set up a webhook from my transactional SMTP provider (Postmark), which emails me (via ActivePieces) if there are bounces/complaints/etc. The spam attacks tend to generate bounces pretty early on, and this system already caught when my fix hadn’t been deployed correctly, and spam was still getting through.

As for the handful that confirmed, I’m unclear about them. Maybe a few real people clicked confirm? I wonder if the IP for the confirmation matches the IP that submitted the sign-up. Looking back, I see a similar pattern with multiple sign-ins on first subscribe, going back a bit, so if it is spam signups it’s been going on longer, at very low volume. I noticed some of those addresses clicked all the links in my newsletter right when it arrived, which seemed suspicious, but a lot of email security software does this. I’m not sure whether to delete these members or not.

Hi everyone,

I’m dealing with a sustained spam attack similar to what others have reported here, but with a frustrating twist: my Cloudflare WAF rules aren’t working at all.

Current situation:

Three self-hosted Ghost blogs (Infomaniak VPS via Coolify) are being hit by bots attacking /members/api/send-magic-link. The pattern matches what @muratcorlu described in the opening post: SMS gateway addresses (@txt.bellmobility.catx@txt.att.net.b@vtext.comllmobility.ca, @txt.att.net, @vtext.com), 95%+ email failures (timeouts), and catastrophic Mailgun reputation damage (delivery rates dropped from 100% to 1.5-3%).

What’s NOT working:

Following @jannis’s recommendation (#18), I deployed Cloudflare WAF rules blocking Tor (country T1) on /members/api/send-magic-link. Result: zero blocked events in Cloudflare Analytics. The spam continues unabated.

After investigation, I discovered why: the bots are bypassing Cloudflare entirely by attacking my server IP directly. My Coolify setup exposes Ghost via Traefik on ports 80/443, making the server accessible from any IP. Even though my domains are proxied through Cloudflare (orange cloud), the bots simply hit the origin IP.

Mailgun logs confirm this: "originating-ip" shows my VPS IP (185.x.x.x), not the bot’s IP. Cloudflare sees zero traffic to the spam endpoint.

Infrastructure questions:

This raises a question about what @Kevin mentioned in the other thread: “This is something better handled at the various network levels above your Ghost instance.”

For self-hosters behind Cloudflare, what does “network level” actually mean when bots can bypass Cloudflare? The two obvious solutions are:

  1. Server-level firewall restricting ports 80/443 to Cloudflare IPs only (15+ firewall rules per VPS)

  2. Cloudflare Tunnel (but this breaks my setup where only subdomains are on the VPS while canonical domains host other apps)

Is this infrastructure hardening standard practice for self-hosted Ghost? If so, why isn’t it documented in Coolify or Ghost security guides? Am I missing something obvious here?

Proposal: Community-maintained blocklist

Separately, I think we need a better long-term solution than each site owner manually updating their blocklist. Inspired by repos like disposable-email-domains, what if we created a community-maintained blocklist of SMS gateway domains on GitHub?

Workflow:

  • Community members submit PRs when they discover new spam domains

  • Ghost users pull the updated list periodically (manually or via webhook/scheduled task)

  • Everyone benefits from collective intelligence instead of playing whack-a-mole individually

This wouldn’t solve the infrastructure problem (bots bypassing Cloudflare), but it would make the blocklist approach more maintainable for everyone. Thoughts? Would anyone be interested in collaborating on this?

Mailgun frustration:

One final note: Mailgun sent zero automated alerts despite weeks of 95%+ failure rates and drastic reputation degradation. I only discovered this by manually checking the dashboard. For others dealing with this, I’d recommend setting up your own monitoring.

Questions:

  1. For self-hosters behind Cloudflare: how are you preventing direct-to-origin attacks? Is firewall hardening or Cloudflare Tunnel the standard approach?

  2. For Ghost(Pro) users: @Kevin, could you share any details about how Ghost(Pro) handles this at the “network level” so self-hosters can learn from it?

  3. Would anyone be interested in collaborating on a GitHub-based community blocklist for SMS gateway domains?

Thanks, Bastien

The spam attack we reported here was not the SMS gateway attack – that was rather something that already happened last year.

This year’s attack (I hope I don’t have to write this every year :upside_down_face: ) was from normal email addresses.

This is somewhat of a red herring. The communication to Mailgun should only come from your server and NOT from the visitors. So, I’d ignore that.

This is the bigger problem.

Ideally, you wouldn’t make that possible at all. The simple (but also circumventable) method would be to have an allow list of IPs in Traefik that only allow Cloudflare IPs: IP Ranges

Circumventable because any Cloudflare worker would also have that IP. So, I could just write a CF worker to attack your site and get through.

The better way: Cloudflare Tunnel · Cloudflare Docs

Yeah, see above. If you have Cloudflare tunnels, I’d just lock down the server completely (apart from SSH, but even that should be behind fail2ban and hardened further).

Not Ghost(Pro), but on Magic Pages all the best practices I shared here are applied. Firewalls (on Cloudflare – since no direct path to the servers exists) are always adapted to what I see in logs across all sites. Self-hosting also means that you monitor all these things yourself. Managed hosting should take care of this for you. Running a server doesn’t mean you set the server (and network stack) up once. It’s constant work and monitoring.

There are quite a few floating around on the forum in other threads already – you could just start by sharing one of these on Github.

Thanks @jannis for the detailed response!

Re: Mailgun originating-ip Got it, that makes sense—I was chasing a red herring there.

Re: Infrastructure You’re right that the Traefik exposure is the core problem. My hesitation with Cloudflare Tunnel comes from my specific setup:

  • Canonical domains host apps outside the VPS (external hosting platforms)

  • Only subdomains (e.g., blog.example.com) are on the VPS

  • Ghost blogs are only accessible via these subdomains

My concern: if I enable Cloudflare Tunnel for these domains, wouldn’t it route all traffic (including canonical domain requests) to the VPS, breaking the external apps?

Or can Cloudflare Tunnel be configured to route only specific subdomains while leaving the canonical domains unaffected? If so, I’d be happy to implement it—I just haven’t found documentation confirming this is possible.

Re: IP allowlist in Traefik I understand this is circumventable via CF Workers, but would it still be worth implementing as a first layer while I figure out the Tunnel configuration?

Re: GitHub blocklist I hear you that lists already exist scattered across forum threads, but I’m not sure a personal GitHub repo would have much authority or adoption. For it to be useful, it would need either:

  1. To live in the official Ghost repo (though I understand Ghost team might not want that maintenance burden)

  2. Ghost to support fetching a community-maintained list from a URL (e.g., "spam.blocklist_url": "``https://ghost.org/community/sms-blocklist.txt``")

Otherwise, it’s just another random list competing with all the others already in forum posts. That said, if infrastructure hardening (Tunnel + firewall) solves the problem properly, the blocklist becomes less critical anyway.

Re: Monitoring Point taken on self-hosting = constant monitoring. This attack was a wake-up call that I need better alerting (which is partly why I’m also reaching out to Mailgun separately about their lack of proactive alerts).

Thanks again for the insights!

Re: VPS-level firewall
Before going the Cloudflare Tunnel route, would it make sense to simply lock down ports 80/443 at the VPS firewall level to only accept Cloudflare IP ranges? This would be simpler to implement and wouldn’t risk breaking my external apps setup. I know it’s circumventable via CF Workers, but is it still worth doing as a practical defense layer?

No, you’re pretty flexible in how you configure these. I don’t use tunnels for the overall Magic Pages infrastructure (but also don’t want to go into too many details of the actual setup to keep attack vectors low), but have it set up for a Grafana dashboard, for example. That runs a specific subdomain of my domain onto a specific port.

Definitely a good first step (but anyone reading here will that also knows your IP will then know how to get in :upside_down_face: – so I wouldn’t trust it as a fully secure setup).

I’d have to disagree a bit here. A list like that wouldn’t need authority – just somebody maintaining it. If somebody searches for it, they’ll find it. I don’t think the Ghost team would want the maintenance burden, as you pointed out.

I do see this as something the community should provide if it’s important enough.

Phew, I’d not hope for anything in that regard. They’ll likely point you towards their webhooks and that’s it. As much as I love them for reliable deliverability, they aren’t known for proactivness in that regard :sweat_smile:

To prevent CDN bypassers, actually there is an undocumented feature built-in to Ghost.

If you set a key on “hostSettings.siteId” configuration value, then Ghost always expects this key on all requests with x-site-id request header. If you add this request header on your Cloudflare Rules, then direct requests to your Ghost instance bypassing Cloudflare will always fail.

3 Likes

Hi @muratcorlu,

Thanks for sharing the hostSettings.siteId approach — I’d like to implement it but I have one blocking question before I do.

The WAF rule only works if the request to /members/api/ carries the x-site-id header. My concern is: who actually sends that header? Does Ghost Portal read the siteId from the site config and automatically inject it into its fetch requests to /members/api/send-magic-link — or does this require additional configuration?

If Portal doesn’t inject the header natively, the WAF rule would block legitimate signups too, which is obviously not what we want.

Could you confirm the exact mechanism? Specifically:

  1. Does Portal.js automatically include x-site-id on every /members/api/ request once hostSettings.siteId is set in config.production.json?
  2. Is there anything else to configure on the Ghost or Cloudflare side?

Thanks in advance — this would be a very clean solution for self-hosters using Cloudflare.

This header is supposed to be used with CDN proxies. All of the traffic (including requests made by Portal.js) comes to your server through Cloudflare. The idea is, you will add this request header on Cloudflare, with a rule. More specifically " Request Header Transform Rule".

1 Like

It seems to be working… How come this miracle solution hasn’t been mentioned anywhere on the forum before?