Shared cPanel servers rarely get blacklisted because the operator misconfigured the relay.
They get blacklisted because one customer's WordPress plugin shelled out, dumped 80,000
sends into Exim's queue, and SORBS picked it up before anyone noticed. Default cPanel
settings will not catch that — outbound caps are off, the nobody user can hit remote
port 25, and there is no SpamAssassin pass on outgoing mail.
This is the layered defence you actually need on a production cPanel box: per-account hourly caps, an Exim ACL that blocks PHP from talking to remote SMTP, an outbound SpamAssassin scan, and the exact triage commands for when the queue is already on fire. For inbound reputation (SPF, DKIM, DMARC, PTR), see the cPanel deliverability checklist — this article assumes that side is already done.
Why the defaults don't catch a spam outbreak
cPanel ships with three permissive defaults that combine badly:
- Max hourly emails per account is
0(unlimited) on most fresh installs. - The
nobodyuser — whichmod_phpand any DSO handler run as — can connect outbound to port 25, so a compromised script bypasses SMTP auth entirely. - SpamAssassin scans inbound only. A script can drop 100 MB of Viagra mail into the queue and Exim will dutifully deliver it.
A compromised PHP file calling mail() or piping to /usr/sbin/sendmail runs as the
account's PHP-FPM user (or nobody), hits Exim's local submission path, and inherits the
account's effective limits. If those limits are 0, your server's IP is on the SBL inside
an hour.
Layer 1: per-account hourly caps in Tweak Settings
In WHM, open Server Configuration → Tweak Settings → Mail. Set the following:
| Setting | Recommended | Why |
|---|---|---|
| Max hourly emails per domain | 200 | Catches a single zone going wild |
| The percentage of email messages a domain may send per hour that may fail (soft and hard bounces) before the domain is blocked from sending | 25 | Real outbound has < 5 % bounce; spam runs 40–80 % |
| Track email origin via X-Source headers | On | Logs the calling script — invaluable for forensics |
| Initial default/catch-all forwarder destination | fail | Stop backscatter from typo'd addresses |
| Restrict outgoing SMTP to root, exim, mailman (FKA SMTP Tweak) | On | The most important toggle on the page |
The last one is the single highest-leverage change on this list. It enables an iptables
rule that blocks outbound TCP 25 from every user except root, exim, and mailman. A
PHP script can still talk to your local Exim — it just can't bypass it by opening a socket
to gmail-smtp-in.l.google.com:25 directly.
Layer 2: per-account caps on the account itself
Tweak Settings sets the default. Override per-account from Home → Account Functions → Modify an Account → Max Hourly Email by Domain Relayed, or in bulk via the CLI:
whmapi1 modifyacct user=customer1 MAX_EMAIL_PER_HOUR=300
whmapi1 modifyacct user=customer1 MAX_DEFER_FAIL_PERCENTAGE=20
For accounts on Reseller plans, set sane ceilings in the package (Packages → Edit a Package) so new accounts inherit them. Resellers can raise the limit for their own clients up to the package maximum — you set the ceiling, they tune below it. This is the right place to enforce policy across hundreds of accounts without per-account clicks.
Layer 3: scan outbound mail with SpamAssassin
Inbound-only SpamAssassin is the cPanel default. Flip it for outbound too in
Service Configuration → Exim Configuration Manager → Advanced Editor, locate the
acl_smtp_data ACL and add a spam clause before the accept verb:
deny
message = This message scored $spam_score points. Sending blocked.
spam = nobody/defer_ok
condition = ${if >{$spam_score_int}{50}{1}{0}}
log_message = Outbound spam rejected for $authenticated_id score=$spam_score
Score 5.0 (the 50 is tenths) is aggressive for outbound — most legitimate marketing mail
will sit between 2 and 4. Tune up if you get false positives from transactional senders,
down if a spam run still slips through.
Hosting-grade tuning is where this gets thankless. If you're spending more than a few hours a quarter chasing outbreaks, the paid option is Imunify Email, which adds AI-trained outbound classification and the ability to quarantine rather than reject. The Imunify360 install guide covers the package; Imunify Email is the same vendor and integrates with the same dashboard.
Layer 4: see the queue and find the culprit
You only get to triage if you can see the outbreak. The two commands every cPanel operator should have in muscle memory:
# Total messages currently queued
exim -bpc
# Top 10 senders by queued message count
exim -bp | awk '/^ *[0-9]+[mhd]/{print $4}' | sort | uniq -c | sort -rn | head
Wire the first one into a Nagios/Prometheus check. A healthy shared cPanel server idles under 200 queued messages; alert at 1,000, page at 5,000. By the time you're at 20,000 you're already on at least one RBL.
The second one tells you which account to suspend. The X-Source headers (enabled in
Layer 1) tell you which script:
exim -Mvh <message-id> | grep -i x-source
# X-Source: /home/customer1/public_html/wp-content/plugins/old-slider/upload.php
# X-Source-Args: /usr/bin/php /home/customer1/public_html/wp-content/...
# X-Source-Dir: /home/customer1/public_html/wp-content/plugins/old-slider
That's your root cause: an old vulnerable plugin in a known location. Restore the file from backup, remove the plugin, push the customer to the cPanel Site Quality Monitoring tools to keep WordPress patched, and move on.
When the outbreak is already happening
Once the queue is in the tens of thousands, the order of operations is fixed:
# 1. Suspend the source account immediately
/scripts/suspendacct customer1 "outbound spam - ticket 12345"
# 2. Freeze every queued message from that account
exim -bp | awk '$6 ~ /<customer1@/{print $3}' | xargs -n1 exim -Mf
# 3. Delete the frozen spam (verify first with -Mvh on a sample)
exim -bp | awk '$6 ~ /<customer1@/{print $3}' | xargs -n1 exim -Mrm
# 4. Restart Exim to clear in-flight deliveries
/scripts/restartsrv_exim
Do not skip step 1. Until the account is suspended, every minute you spend clearing the queue is a minute the compromised script is generating more mail. Suspend first, triage second.
After the queue is clean, check the major RBLs (Spamhaus SBL/CSS, SORBS, Barracuda) and file delisting requests for any that picked you up. Most of them auto-delist within 24 hours once the source stops, but Spamhaus SBL requires a manual request and a clear explanation of what changed.
Bonus: stop the nobody user sending mail at all
If you're on suPHP, PHP-FPM, or LSAPI handlers, every PHP request runs as the account
user, not nobody. The nobody user has no legitimate reason to send mail and you can
block it entirely:
# Add nobody to the Exim sender blocklist
echo "nobody" >> /etc/blockcpsmtpgid
/scripts/restartsrv_exim
This will not affect any modern cPanel server — nobody mail is a 2012 problem. But
catching the one rogue CGI script that still runs as nobody is a 30-second hardening
win. For the broader picture of which handler runs as which user, the
PHP handlers on cPanel comparison lays it out.
How do I stop one cPanel account from sending spam?+
Why does my cPanel server keep getting blacklisted even with outbound limits?+
Does the nobody user need outbound SMTP on any modern cPanel server?+
How do I check what's in the Exim queue right now?+
Will enabling outbound SpamAssassin slow down legitimate mail?+
Is the WHM 'Restrict outgoing SMTP' option safe to enable on an existing server?+
Next steps
- Email deliverability on cPanel: SPF, DKIM, DMARC, MTA-STS covers the inbound-reputation half of the same problem.
- Install Imunify360 on cPanel — Imunify Email is the same vendor's outbound-spam classifier and bolts onto this stack.
- PHP handlers on cPanel compared — moving off DSO/
nobodyremoves a whole class of outbound-spam vectors.