cPanel Server Under DDoS: Find the Vhost and Block It
Load spikes, the box crawls, and every domain on the server looks equally guilty. Here is how to find the one that is not, and stop it, without touching the others.
Load average climbs past 30, WHM is slow to respond, and every site on the box feels the pain even though only one of them is actually the target. This is what a cPanel server DDoS attack usually looks like at the application layer: one vhost gets flooded with application-level requests, Apache or LiteSpeed spawns workers to handle them, and every other tenant on the server starves for the same CPU and memory. The fix has nothing to do with any individual site's code. It is entirely about finding which vhost is absorbing the traffic and cutting off the source before the whole box falls over.
Confirm it is one vhost, not the whole server
Start with load and see whether one process is dominating, then find which domain owns it. top sorted by CPU usually shows a stack of httpd or lsphp workers all at high CPU, which tells you the server is busy serving requests, not that anything is broken. What it will not tell you is which site those requests belong to.
uptime
ps aux --sort=-%cpu | head -15
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
The ss count shows how many connections are in each TCP state. A large SYN_RECV or ESTABLISHED count is the first real signal.
The actual answer is in the per-domain access logs. On cPanel these live at /usr/local/apache/domlogs/<domain> or, on newer installs with the combined logs feature, under /etc/apache2/logs/domlogs/. Count requests per domain in the last few minutes across all of them and the flooded one is obvious, it will be an order of magnitude above the rest:
for f in /usr/local/apache/domlogs/*; do
n=$(tail -n 5000 "$f" 2>/dev/null | wc -l)
echo "$n $f"
done | sort -rn | head -10
Find the source inside that vhost's log
Once you have the domain, the same domlog tells you whether this is a few IPs hitting hard or many IPs hitting lightly. That distinction decides which tool you reach for next.
tail -n 10000 /usr/local/apache/domlogs/example.com | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
A handful of IPs each responsible for thousands of hits is the easy case, block them and load drops immediately. If instead you see hundreds of distinct IPs each only a few requests deep, that is a distributed low-and-slow pattern designed specifically to stay under per-IP rate limits. It is worth also checking which URL is being hit and what response codes it is getting, since a flood of requests all returning 500 means the application itself is struggling under the load in a way that compounds the problem:
tail -n 10000 /usr/local/apache/domlogs/example.com | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head -20
Block the source at the server
On a stock cPanel and WHM install, CSF (ConfigServer Security and Firewall) is the fastest way to drop traffic before it reaches Apache or LiteSpeed at all. Blocking here is cheaper than letting the web server process and reject the request, because the connection never gets that far.
csf -d 203.0.113.44 "layer 7 flood on example.com"
For a handful of offenders, script it from the list you already produced:
tail -n 10000 /usr/local/apache/domlogs/example.com \
| awk '{print $1}' | sort | uniq -c | sort -rn | head -10 \
| awk '{print $2}' \
| xargs -I{} csf -d {} "layer 7 flood on example.com"
For the distributed low-and-slow pattern, individual blocks do not scale. Two things help more: mod_evasive, which rate limits by IP automatically and blocks anything that exceeds a request threshold within a short window, and CSF's own connection tracking (CT_LIMIT in /etc/csf/csf.conf), which caps simultaneous connections per IP regardless of how many distinct IPs are involved.
apache2ctl -M 2>/dev/null | grep evasive
ls /etc/apache2/mods-enabled/ | grep evasive
Confirm it actually worked
Blocking IPs and walking away is how the same attack comes back an hour later from a fresh range. Re-check load, re-check the connection count, and re-check that the domlog for the target vhost has actually gone quiet, not just that the commands you ran exited without an error:
uptime
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
tail -n 200 /usr/local/apache/domlogs/example.com | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
If load has not dropped within a couple of minutes of blocking, the attack has likely shifted source IPs, which is common with rented botnets. Repeat the domlog count rather than assuming the first round of blocks was the end of it.
That whole sequence, find the vhost, count IPs, check the URL pattern, block, re-check, is exactly what an operator like BashPilot runs when asked to check a server for an active attack. Here is the same triage as one conversation, with the details changed to a generic example:
See how BashPilot handles this
The server is under active attack from multiple vectors at once. One IP is responsible for a sustained flood against a single vhost, a separate group of IPs is running a distributed brute force against wp-login.php across several domains, and a third, much larger group is making only two or three requests each against the site root, the classic low-and-slow pattern built to stay under per-IP rate limits.
- Single-IP flood: one address responsible for several thousand requests to one path on
bashpilot.com, all successful, consistent with a scraper or API abuser rather than a vulnerability scan. - Distributed brute force: over 40 distinct IPs hitting
/wp-login.phpacross more than 50 domains on this server, several returning 302 redirects, worth checking for a successful login. - Low-and-slow: 242 distinct IPs each averaging about two requests to
/, designed specifically to evade simple rate limiting.
| Metric | Last 60 min |
|---|---|
| Total requests | 33,856 |
| Distinct IPs | 1,443 |
| Error rate (4xx + 5xx) | 51.2% |
| 5xx errors | 1,704 |
This will block the single IP responsible for the flood against bashpilot.com and re-check server load once applied. Approve to continue.
csf -d 203.0.113.44 "layer 7 flood on bashpilot.com"
Try this on one of your own servers.
Start free trialWhat this does and does not fix
Everything above stops traffic once it has already reached your server's network interface. It does nothing about the pipe itself being saturated, which is a true volumetric attack and has to be absorbed upstream, at Cloudflare, your provider's DDoS scrubbing, or a CDN sitting in front of the box. If uptime shows normal load but the server is still unreachable, that is a bandwidth problem, not a vhost problem, and no amount of CSF blocking on the origin server will touch it. The triage in this article is specifically for the case where the server is reachable, but struggling, because it is doing real work processing a flood of application-level requests.
Why this is worth automating
Doing that correctly under pressure, at 2am, across a server hosting forty unrelated customer sites, is exactly where mistakes happen: blocking a legitimate IP, missing that the attack moved domains, or declaring victory before load has actually recovered. That is the part worth handing off, not the judgment about which sites matter most.