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.

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).

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

CheckCommandExpected
Key login as your userssh sammy@your_server_ipShell, no server password prompt
Root login blockedssh root@your_server_ipPermission denied (publickey,...)
Firewallsudo firewall-cmd --list-servicesOnly the services you need
Fail2Bansudo fail2ban-client status sshdJail listed
SELinuxgetenforceEnforcing
Automatic updatessystemctl is-active dnf-automatic.timeractive
TimetimedatectlSystem 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.