A new server is probed by automated SSH scanners within minutes of getting a public IP address. In this tutorial you will apply the security baseline for Rocky Linux 9: updated packages, a non-root administrator in the wheel group, key-only SSH with root login disabled, firewalld, Fail2Ban from EPEL, SELinux in enforcing mode and automatic security updates with dnf-automatic. The same commands work on AlmaLinux 9, RHEL 9 and CentOS Stream 9; CentOS Linux 7 and 8 are end of life and should be migrated rather than hardened.
Prerequisites
To follow this guide you need:
- A new server running Rocky Linux 9, for example a CubePath VPS.
- Root access over SSH and the server's IP address, shown as
your_server_ip. - An SSH key pair on your local computer. If you do not have one, create it with
ssh-keygen -t ed25519. - Access to the server's web console (on CubePath, the VNC console in the dashboard) in case an SSH or firewall change locks you out.
ImportantKeep your current SSH session open until you have confirmed, from a second terminal, that you can still log in after each SSH or firewall change.
Step 1 - Updating the system
Log in as root and install all pending updates first:
ssh root@your_server_ip
dnf upgrade -y
Check whether the update requires a reboot (for example after a new kernel or glibc). The needs-restarting command comes from dnf-plugins-core, which is installed on standard images:
dnf needs-restarting -r
Core libraries or services have been updated since boot-up:
* kernel
Reboot is required to fully utilize these updates.
More information: https://access.redhat.com/solutions/27943
If it says a reboot is required, run reboot, wait a minute, and log back in as root. If it prints No core libraries or services have been updated since boot-up, continue.
Step 2 - Creating a non-root administrative user
Daily work as root turns every mistake into a system-wide one. Create a regular user; this guide uses sammy, replace it with your own your_user:
useradd -m sammy
passwd sammy
Choose a strong password: you will type it for sudo even after SSH switches to keys.
On RHEL-based systems, members of the wheel group may run any command with sudo (this rule is already enabled in /etc/sudoers). Add the user to it:
usermod -aG wheel sammy
Verify:
su - sammy -c 'sudo whoami'
[sudo] password for sammy:
root
Step 3 - Installing your SSH key for the new user
From your local computer, copy your public key to the new account:
ssh-copy-id sammy@your_server_ip
If your key is already in root's authorized_keys, you can copy it on the server instead. restorecon resets the SELinux labels, which sshd checks before reading the file:
mkdir -p /home/sammy/.ssh
cp /root/.ssh/authorized_keys /home/sammy/.ssh/
chown -R sammy:sammy /home/sammy/.ssh
chmod 700 /home/sammy/.ssh
chmod 600 /home/sammy/.ssh/authorized_keys
restorecon -R /home/sammy/.ssh
Open a second local terminal and log in as the new user:
ssh sammy@your_server_ip
You should get a shell without the server password prompt. Run sudo -v to confirm sudo works, and use this account from now on.
Step 4 - Hardening the SSH server
Rocky Linux 9 reads drop-in files from /etc/ssh/sshd_config.d/ before the rest of /etc/ssh/sshd_config, in alphabetical order, and keeps the first value it finds for each option. The installer may create 01-permitrootlogin.conf to allow root password logins, so name your file 00-... to make sure it wins:
sudo vi /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers sammy
PermitRootLogin no blocks root entirely, the two authentication lines leave keys as the only method, MaxAuthTries and LoginGraceTime limit how long a bot can keep a connection busy, and AllowUsers lists who may log in at all (add other administrators separated by spaces).
TipIf you prefer
nano, install it withsudo dnf install -y nano. Minimal Rocky images only shipvi.
Test the configuration and look at the effective values:
sudo sshd -t
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowusers)'
permitrootlogin no
passwordauthentication no
allowusers sammy
Restart the service. On RHEL-based systems it is called sshd:
sudo systemctl restart sshd
From your second terminal, check that ssh sammy@your_server_ip still works and that root is refused:
ssh root@your_server_ip
[email protected]: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
Step 5 - Configuring firewalld
firewalld is installed and running by default on Rocky Linux 9. Confirm it and see what the active zone allows:
sudo firewall-cmd --state
sudo firewall-cmd --list-all
running
public (active)
target: default
icmp-block-inversion: no
interfaces: eth0
sources:
services: cockpit dhcpv6-client ssh
ports:
...
If firewalld is not running, install and start it with sudo dnf install -y firewalld and sudo systemctl enable --now firewalld.
The default public zone already allows SSH and rejects everything else. Remove services you do not use; cockpit (the web console on port 9090) is a common one:
sudo firewall-cmd --permanent --remove-service=cockpit
If the server will host websites, allow HTTP and HTTPS:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
--permanent only changes the saved configuration. Load it into the running firewall and check again:
sudo firewall-cmd --reload
sudo firewall-cmd --list-services
dhcpv6-client http https ssh
Step 6 - Installing Fail2Ban
Fail2Ban bans IP addresses that fail to authenticate repeatedly. It is packaged in EPEL (Extra Packages for Enterprise Linux), which Rocky Linux ships as a package in its extras repository:
sudo dnf install -y epel-release
sudo dnf install -y fail2ban
The fail2ban package pulls in fail2ban-firewalld, which configures bans as firewalld rich rules, so no firewall setting is needed. Create your local configuration; never edit jail.conf, because updates replace it:
sudo vi /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
backend = systemd
backend = systemd reads SSH failures from the journal. If you always manage the server from a fixed IP address, add ignoreip = 127.0.0.1/8 ::1 your_ip under [DEFAULT] so you cannot ban yourself.
Enable the service and check the jail:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Banned addresses show up in the firewall as rich rules:
sudo firewall-cmd --list-rich-rules
To release an address, run sudo fail2ban-client set sshd unbanip 198.51.100.7.
Step 7 - Keeping SELinux in enforcing mode
SELinux confines each service to the files, ports and actions its policy allows, so a compromised web server cannot, for example, read SSH keys. Rocky Linux enables it in enforcing mode by default. Check it:
sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
...
If the current mode is permissive or disabled, set SELINUX=enforcing in /etc/selinux/config. When switching from disabled, relabel the file system on the next boot first:
sudo touch /.autorelabel
sudo reboot
Leave SELinux enforcing and fix denials instead of turning it off. Install the management tools:
sudo dnf install -y policycoreutils-python-utils setroubleshoot-server
The three fixes you will need most often are:
- A service on a non-default port, for example SSH on 2222:
sudo semanage port -a -t ssh_port_t -p tcp 2222 - Files created or moved with the wrong label:
sudo restorecon -Rv /path/to/directory - A feature controlled by a boolean, for example letting a web server proxy to a backend:
sudo setsebool -P httpd_can_network_connect on
When something is blocked, read the recent denials and their explanation:
sudo ausearch -m AVC -ts recent
sudo sealert -a /var/log/audit/audit.log
Step 8 - Enabling automatic security updates
dnf-automatic downloads and installs updates on a systemd timer:
sudo dnf install -y dnf-automatic
Edit its configuration so it installs security updates only:
sudo vi /etc/dnf/automatic.conf
In the [commands] section, set these two values:
[commands]
upgrade_type = security
apply_updates = yes
Enable the timer:
sudo systemctl enable --now dnf-automatic.timer
systemctl list-timers dnf-automatic.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Fri 2026-09-25 06:32:11 UTC 19h left - - dnf-automatic.timer dnf-automatic.service
dnf-automatic does not reboot the server. Run sudo dnf needs-restarting -r from time to time, or during a maintenance window, to see whether a new kernel is waiting. Past runs are in sudo journalctl -u dnf-automatic.service.
Step 9 - Checking time synchronization
Rocky Linux uses chrony for NTP, enabled by default. Check the clock and the time sources:
timedatectl
chronyc tracking
timedatectl should show System clock synchronized: yes and NTP service: active. If not, run sudo dnf install -y chrony and sudo systemctl enable --now chronyd. UTC is a sensible timezone for servers; to change it use sudo timedatectl set-timezone Europe/Madrid.
Step 10 - Reviewing listening services
List everything listening on the network:
sudo ss -tulpn
On a new server you should see sshd on port 22, chronyd on 127.0.0.1:323, and possibly systemd holding port 9090 for cockpit.socket. If you do not use Cockpit, disable it:
sudo systemctl disable --now cockpit.socket
Disable other services only when you know what they do: sudo systemctl disable --now service_name.
Verification
| Check | Command | Expected |
|---|---|---|
| Key login as your user | ssh sammy@your_server_ip | Shell, no server password prompt |
| Root login blocked | ssh root@your_server_ip | Permission denied (publickey,...) |
| Firewall | sudo firewall-cmd --list-services | Only the services you need |
| Fail2Ban | sudo fail2ban-client status sshd | Jail listed |
| SELinux | getenforce | Enforcing |
| Automatic updates | systemctl is-active dnf-automatic.timer | active |
| Time | timedatectl | System clock synchronized: yes |
Troubleshooting
Locked out after the SSH change: log in through the web console, check AllowUsers in /etc/ssh/sshd_config.d/00-hardening.conf, run sudo sshd -t and sudo systemctl restart sshd. Also make sure /home/sammy/.ssh has mode 700, authorized_keys has 600, and the labels are correct (restorecon -Rv /home/sammy/.ssh).
sshd fails to start on a new port: SELinux only allows sshd on ports labelled ssh_port_t. Add the port with semanage port -a as shown in Step 7, and open it in firewalld.
No match for argument: fail2ban: EPEL is not enabled. Run sudo dnf install -y epel-release and try again.
A service works with setenforce 0 but not in enforcing mode: that is an SELinux denial. Read it with sudo sealert -a /var/log/audit/audit.log, apply the suggested boolean, label or port fix, and switch back with sudo setenforce 1.
Conclusion
Your Rocky Linux 9 server now runs current packages, has a dedicated sudo user, accepts only SSH keys, exposes only the services you chose through firewalld, bans brute-force sources, keeps SELinux enforcing and installs security updates automatically. Next, consider moving SSH to a non-default port to reduce log noise, setting up off-server backups, and installing your application stack while opening only its ports.
