Before you touch anything
Open a second SSH session and leave it connected. Every change below is applied with sshd -t (config test) before a reload, and a reload never drops existing sessions, so if you lock yourself out you still have the first window to undo it.
Table of Contents
Check that the server is actually using sshd_config and not a drop-in you did not know about:
sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|^port|pubkeyauthentication'
ls /etc/ssh/sshd_config.d/
On AlmaLinux 9 the file /etc/ssh/sshd_config.d/50-redhat.conf exists and is read before the main file. Anything you set in the main file that conflicts with it loses, because sshd keeps the first value it sees. Put your hardening in its own drop-in with a lower number so it wins.
1. Install your key, then disable passwords
From your workstation:
ssh-keygen -t ed25519 -C "you@workstation"
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@server
Log in once with the key to prove it works. Then create /etc/ssh/sshd_config.d/10-hardening.conf:
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
prohibit-password lets root in with a key only, which is what most cPanel and DirectAdmin servers need. If you have a sudo user and never need root over SSH, use PermitRootLogin no instead. Test and reload:
sshd -t && systemctl reload sshd
Try a password login from another terminal; it should be refused immediately.
2. Change the port without fighting SELinux
Moving off port 22 does not make the server secure, but it removes 95% of the automated noise from your logs and from your brute-force blocker. Pick a port above 1024 that nothing else uses, for example 2222 is too common; 48222 is fine.
SELinux only allows sshd to bind to ports labelled ssh_port_t. Add the label first or sshd will fail to start:
dnf -y install policycoreutils-python-utils
semanage port -a -t ssh_port_t -p tcp 48222
firewall-cmd --permanent --add-port=48222/tcp && firewall-cmd --reload
echo "Port 48222" >> /etc/ssh/sshd_config.d/10-hardening.conf
sshd -t && systemctl restart sshd
Connect on the new port in a new terminal before you close the old one. Only then remove port 22 from the firewall (firewall-cmd --permanent --remove-service=ssh). If you use CSF instead of firewalld, add the port to TCP_IN in /etc/csf/csf.conf and run csf -r.
3. Rate-limit and ban
With passwords off, brute force cannot succeed, but it still costs CPU and fills /var/log/secure. Use whichever blocker the server already has: CSF+LFD (LF_SSHD = "5" in csf.conf), cPHulk on cPanel, or fail2ban on a plain box:
dnf -y install fail2ban
cat > /etc/fail2ban/jail.d/sshd.local <<'EOF'
[sshd]
enabled = true
port = 48222
maxretry = 4
findtime = 10m
bantime = 24h
EOF
systemctl enable --now fail2ban
Do not run two blockers at once; they fight over iptables chains.
4. Restrict who can log in
If only two people should ever SSH in, say so:
AllowUsers root deploy
Add it to the same drop-in. Anyone else gets “Permission denied” before authentication even starts. For root, also trim /root/.ssh/authorized_keys — old keys from previous admins and providers’ automation are the most common leftover.
Verify
sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|^port|maxauthtries|allowusers'
ss -ltnp | grep sshd
grep -c 'Failed password' /var/log/secure
The last number should stop growing within an hour. The server-security-audit script checks all of the above and flags anything that drifts later.
Harden SSH AlmaLinux 9 at a glance

Official documentation: AlmaLinux wiki, Linux man pages.
Related guides: KernelCare on cPanel and DirectAdmin servers: setup, verification and rollback · Replacing cxs: malware scanning with LMD (maldet), ClamAV and ImunifyAV on hosting servers · Incident response after a cPanel root-escalation CVE: rotating keys, hunting .sorry, auditing sessions.