TL;DR
The first hour on a fresh AlmaLinux 10 or Rocky Linux 10 server, condensed: update, create a sudo user, move SSH to keys only, keep firewalld and SELinux exactly as they are. Every step below has the expected output and the recovery path.
$ sudo dnf -y upgrade $ sudo useradd -m -G wheel deploy && sudo passwd deploy $ ssh-copy-id [email protected] $ printf 'PermitRootLogin no\nPasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/50-hardening.conf $ sudo sshd -t && sudo systemctl restart sshd $ sudo firewall-cmd --permanent --remove-service=cockpit && sudo firewall-cmd --reload $ getenforce
Step 0: a server to practice on, and the CPU detail nobody tells you
Disclosure: the Vultr link below is a referral. You get 300 dollars of trial credit for 30 days, this site gets a commission if you stay. That is the whole arrangement.
If you are doing this on a work machine, skip ahead. If you want a disposable box to break, I spin these labs up on Vultr's 300 dollar trial credit: deploy the smallest instance, follow this guide, destroy it, repeat. The credit covers a month of that many times over, and practicing a lockout recovery on a server you can delete is worth more than reading about one.
Before you pick a distribution, know this: Rocky Linux 10 requires an x86-64-v3 CPU (Intel Haswell from 2013 onward, or equivalent AMD), while AlmaLinux 10 also ships a separate x86-64-v2 build for hardware back to roughly 2008. Any current cloud instance is v3, but older home servers and some budget VPS plans are not. Check before you download:
$ /lib64/ld-linux-x86-64.so.2 --help | grep supported
x86-64-v3 (supported, searched)
x86-64-v2 (supported, searched)
If v3 is missing from that list, AlmaLinux's v2 build is your distribution and the rest of this guide is identical. Both are rebuilds of RHEL 10 on kernel 6.12, and everything below works the same on either.
Step 1: update and confirm what you are on
$ sudo dnf -y upgrade $ grep PRETTY /etc/os-release PRETTY_NAME="AlmaLinux 10.2 (Lavender Lion)" $ dnf --version | head -1 4.20.0
Yes, dnf 4: unlike Fedora, the RHEL 10 family stays on the dnf 4 line for its lifetime. Same commands you know, no dnf5 surprises.
While you are here, give the machine a name and a clock you can trust in logs. Time sync via chrony is already on by default in this family; naming is on you:
$ sudo hostnamectl set-hostname lab01.example.internal
$ sudo timedatectl set-timezone America/Sao_Paulo
$ timedatectl
Local time: Fri 2026-07-17 22:10:03 -03
System clock synchronized: yes
NTP service: active
Named hosts and synchronized clocks sound cosmetic until the first time you correlate an auth log across three servers at 2 a.m. Do it while it costs nothing.
Step 2: a sudo user, because root is not a daily driver
$ sudo useradd -m -G wheel deploy
$ sudo passwd deploy
$ id deploy
uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),10(wheel)
The wheel group is sudo on the RHEL family, enabled out of the box by the %wheel ALL=(ALL) ALL line in sudoers. One honest gotcha from testing this on a minimal image: sudo itself may not be installed on stripped down cloud images and containers. If sudo comes back command not found, you are root anyway on first login, so dnf -y install sudo and carry on.
Step 3: SSH keys only, with a config you can roll back
From your own machine, put your key on the server before touching the SSH daemon:
$ ssh-copy-id [email protected]
$ ssh [email protected] # confirm the key works BEFORE hardening
Now harden. RHEL 10 family ships OpenSSH 9.9, and the clean way is a drop-in file, not editing the main config:
$ printf 'PermitRootLogin no\nPasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/50-hardening.conf $ sudo sshd -t $ sudo systemctl restart sshd
sshd -t printing nothing is the good outcome: the config parses. The iron rule: keep your current SSH session open while you restart the daemon and test a fresh login from a second terminal. If the fresh login fails, you still hold a live session to undo the change with sudo rm /etc/ssh/sshd_config.d/50-hardening.conf and a restart.
Step 4: firewalld stays on, and gets smaller
Do not disable the firewall, trim it. See what the default zone actually exposes:
$ sudo firewall-cmd --list-services
cockpit dhcpv6-client ssh
SSH stays, obviously. Cockpit is the web admin console on port 9090; if you are not using it, it is attack surface:
$ sudo firewall-cmd --permanent --remove-service=cockpit
$ sudo firewall-cmd --reload
$ sudo firewall-cmd --list-services
dhcpv6-client ssh
When you later add a web server, you add its service the same way: --add-service=http --add-service=https, permanent, reload. The habit to build is that every open port is a decision you made, not a default you inherited.
Step 5: SELinux stays exactly where it is
$ getenforce
Enforcing
Half the tutorials you will find end with "disable SELinux to avoid problems". That advice is how test boxes become breach writeups, and it is the line this site will never cross. SELinux denials are not mysteries, they are log entries with fixes: relabel the file, flip the boolean, add the port. When a denial bites you in a real deployment, ausearch -m AVC -ts recent tells you exactly what was blocked, and fixing that one thing takes five minutes. Leave it Enforcing today and your future self inherits a server where security was never traded for convenience.
Step 6: security updates without you
$ sudo dnf -y install dnf-automatic $ sudo systemctl enable --now dnf-automatic.timer
The default configuration downloads and applies updates on a daily timer; set apply_updates = yes under [commands] in /etc/dnf/automatic.conf if your build ships it off. For a lab or a small server, automatic security patching beats a forgotten server every time. Reboots for kernel updates remain yours to schedule.
If this fails
- Locked out after step 3: use your provider's web console (every VPS panel has one), log in locally as root, remove
/etc/ssh/sshd_config.d/50-hardening.conf, restart sshd, and redo the step with the key tested first. sudo: command not foundon a minimal image: you are root on first login anyway;dnf -y install sudoand re-run step 2.firewall-cmdanswers with a DBUS error: firewalld is not running yet (common in containers and kickstart scripts).sudo systemctl enable --now firewalld, or usefirewall-offline-cmdin build scripts.- Rocky Linux 10 installer or image refuses to boot on older hardware: that is the x86-64-v3 requirement from step 0. Switch to AlmaLinux 10's v2 build, everything else in this guide is unchanged.
Wrap up
Twenty minutes, six steps, and the server now fails safe: no root over SSH, no passwords to brute force, a firewall you can enumerate from memory, SELinux still doing its job, and patches applying themselves. This is the baseline every other guide on this site builds on. The next ones go deeper into the two tools people are most tempted to turn off, firewalld zones and SELinux troubleshooting, in the same spirit as everything this domain has stood for since 2003: the right way, tested, with the output to prove it.