Securing a cPanel Server: The Checklist I Actually Run
SSH keys, two-factor, CSF rules, fail2ban, AutoSSL with HSTS, and the backup habit that matters more than any of them. The hardening order I follow on every cPanel server I take over.
If you run cPanel/WHM servers for a living, you learn quickly that most compromises are not exotic. They are the obvious door left open: a default SSH port with password auth, an unpatched plugin, a world-writable script, or an admin who reused a password.
This is the order I work through when I harden a server. Nothing here is secret — it is just applied consistently, which is the part most people skip.
Test anything below on a staging box first. Firewall and SSH changes can lock you out of a production machine in seconds.
The five-minute triage#
Before changing anything, find out what you are working with.
# what is listening, and on what interface?
ss -tulnp
# which services start at boot?
systemctl list-unit-files --state=enabled --type=service
# what is actually installed and how old is it?
cat /etc/redhat-release 2>/dev/null || cat /etc/os-release
rpm -qa --last 2>/dev/null | head -20
# is the package stack current?
yum check-update -q | head -30 # RHEL / AlmaLinux / CentOS
apt list --upgradable 2>/dev/null | head -30 # Debian / Ubuntu
# who is logged in, and who was?
w
last -n 20Write down what you find. You want a before-and-after, not a vague feeling that it is "more secure now".
1. Patch the whole stack, not just the OS#
cPanel installs its own copies of Apache, PHP, MySQL and Exim. An up-to-date OS with an out-of-date EasyApache profile is still exposed.
# OS packages
yum -y update # or: apt update && apt upgrade -y
# cPanel itself
/scripts/upcp
# see which Apache / PHP / Exim versions are in play
/scripts/check_cpanel_rpms --list-only 2>/dev/null | head
httpd -v
php -v
exim --version | head -1Then in WHM: EasyApache 4 → review the PHP versions actually in use. PHP 7.x is end of life — if any account still runs it, that is your first conversation with the customer, not your last.
2. Lock down SSH#
This is the single highest-value change. Password authentication against port 22 is brute-forced continuously; you will see it in your logs the same day you take over a server.
# generate a key on YOUR machine, not the server
ssh-keygen -t ed25519 -C "sameur@$(hostname)"
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@your-server-ip
# confirm key login works BEFORE touching sshd_config
ssh -i ~/.ssh/id_ed25519 root@your-server-ipOnly then edit the server:
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)# /etc/ssh/sshd_config
PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
ChallengeResponseAuthentication no
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers root deployer# validate first — a syntax error here can lock you out
sshd -t && systemctl restart sshdKeep a second terminal open while you test. Change the port only if it fits your workflow — it reduces log noise, not real risk, and it breaks muscle memory at 3 a.m.
3. Two-factor on every privileged account#
| Where | How |
|---|---|
| WHM root | WHM → Two-Factor Authentication → enable, then enrol your own account |
| cPanel users | WHM → Manage Two-Factor Authentication → require for all |
| SSH | Google Authenticator PAM module, or keep it key-only |
| Billing/domain registrar | Whatever the provider offers — this one gets forgotten and it owns your DNS |
The registrar is the one people lose. If an attacker takes the domain, they can point it anywhere and your server hardening becomes irrelevant.
4. Firewall: deny everything, then allow what you need#
Most cPanel servers run CSF. If you are on a plain install, firewalld or ufw will do.
# CSF — the ports that actually need to be open
# /etc/csf/csf.conf
TCP_IN = "20,21,22,25,53,80,110,143,443,465,587,993,995,2082,2083,2086,2087,2095,2096"
TCP_OUT = "20,21,22,25,53,80,110,143,443,465,587,993,995,2082,2083,2086,2087,2095,2096"
LF_SSHD = "5" # block after 5 failed SSH logins
LF_SSHD_PERM = "3600"
LF_PERMBLOCK = "1"
CT_LIMIT = "150" # connection tracking
TESTING = "0"csf -r # reload
csf -l # confirm it is running
csf -g 1.2.3.4 # check whether an IP is blockedTwo rules worth enforcing:
- Restrict WHM ports (2086/2087) to known IPs if your team has fixed addresses.
- Never open MySQL (3306) to the world. Applications on other hosts should connect over a VPN or an SSH tunnel.
# quick sanity check from an outside network
nc -zv your-server-ip 3306 # this MUST fail
nc -zv your-server-ip 443 # this should succeed5. fail2ban for everything else#
CSF's LF_* options already cover a lot. Where you want more, fail2ban is the tool:
yum -y install fail2ban # or: apt install fail2ban# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
[sshd]
enabled = true
port = ssh
[exim-sasl]
enabled = truesystemctl enable --now fail2ban
fail2ban-client status sshdCheck fail2ban-client status sshd a week later. If the banned count is in the thousands, that is normal — it means the filter is doing its job, not that you are under a targeted attack.
6. TLS and HTTP hardening#
Run AutoSSL (WHM → SSL/TLS → Manage AutoSSL Coverage) so every account gets a valid certificate, then force the redirect and add headers.
# /etc/apache2/conf.d/userdata/std/2_4/username/domain.com/hardening.conf
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"# cPanel rebuilds its Apache config from templates — always do this after edits
/scripts/rebuildhttpdconf
systemctl reload httpdEnable HSTS only after you are certain every subdomain has a working certificate. Turning it on with a broken subdomain means that subdomain stays broken for as long as max-age says.
If the domain sits behind Cloudflare, set SSL/TLS mode to Full (strict) — Flexible mode sends the origin hop in plaintext, which defeats the certificate you just installed.
7. Remove what you do not use#
Every running service is a thing that can be exploited.
# services listening that you may not need
ss -tulnp | awk 'NR>1{print $1, $5, $7}' | sort -uCommon candidates on a hosting box:
- Unused PHP versions in EasyApache
telnet,rsh,tftp— uninstall, do not just disable- Default sample or test applications shipped by a provider
- phpMyAdmin exposed on a public IP with no auth layer in front
- Old cron jobs left by a previous admin:
for u in $(cut -d: -f1 /etc/passwd); do crontab -l -u $u 2>/dev/null | sed "s|^|$u: |"; done
Also in WHM → Tweak Settings: disable directory indexes, turn off mod_status public access, and restrict cgi-sys where you can.
8. Backups you have actually restored#
A backup you have never restored is a rumour.
# JetBackup 5 — list what exists
jetbackup --list-backups 2>/dev/null | head
# or cPanel's native backup
/scripts/cpbackup --force
ls -lh /backup/$(date +%Y-%m-%d)/ 2>/dev/nullWhat I insist on:
- Off-server destination. A backup on the same disk dies with the disk — and with the ransomware.
- A scheduled restore test. Once a month, restore one account to a scratch directory and check the files are there.
- Retention you can defend. 7 daily, 4 weekly, 3 monthly is a reasonable default.
- Database dumps included, not just home directories.
9. Know where to look when something goes wrong#
| Symptom | First place to look |
|---|---|
| SSH brute force | /var/log/secure (RHEL) · /var/log/auth.log (Debian) |
| Site defaced or strange files | grep -rIl "eval(base64_decode" /home/*/public_html |
| Outbound spam | /var/log/exim_mainlog · exim -bp |
| Disk full | du -sh /home/* \| sort -h \| tail then du -sh /var/log/* |
| High load, unknown cause | atop -r for history · ps aux --sort=-%cpu \| head |
| Something changed at 3 a.m. | grep CRON /var/log/cron and /var/log/messages |
# a quick malware sweep of web roots
find /home/*/public_html -type f -newermt "-2 days" -name "*.php" | head -50
grep -rIl -e "eval(base64_decode" -e "gzinflate(str_rot13" /home/*/public_html 2>/dev/null | headThe part that actually matters#
Run this whole list, and then put a date in your calendar to do it again next quarter. Hardening is not a project you finish — it is maintenance, exactly like patching or backups. The servers that get compromised are almost never the ones with the weakest configuration on day one. They are the ones nobody looked at for two years.
If you want a starting point, work top to bottom through sections 1 to 4. That alone removes the majority of the ways a shared hosting server gets owned.