Back to blog
rhel

RHEL 9 / AlmaLinux / Rocky server hardening checklist

A copy-pasteable hardening checklist for the RHEL 9 family: SSH lockdown, firewalld zones, dnf-automatic security updates, keeping SELinux enforcing (really), sysctl settings, and auditd - plus the drift problem no checklist survives.

Sandeep Sidhu

· Founder, AlertKick

3 min read
RHEL 9 / AlmaLinux / Rocky server hardening checklist

The RHEL 9 family - Red Hat, AlmaLinux, Rocky - starts from a stronger security posture than most distros: SELinux enforcing, firewalld running, auditd on by default. The hardening job here is less “install the missing pieces” and more “don’t dismantle the good defaults, and lock the doors that ship open”. Everything below works identically on all three; the reasoning behind the shared controls lives in our Ubuntu guide, and this page focuses on what the RHEL family does differently. (Distro-neutral version with a verify command per step: the Linux server security checklist.)

1. A named user with wheel

sudo useradd -m deploy && sudo passwd deploy
sudo usermod -aG wheel deploy     # wheel, not sudo - RHEL's admin group
# verify sudo works as deploy BEFORE touching sshd

2. SSH: keys only, root locked out

# workstation: ssh-keygen -t ed25519 && ssh-copy-id deploy@server
sudo tee /etc/ssh/sshd_config.d/90-hardening.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
LoginGraceTime 20
AllowUsers deploy
EOF
sudo sshd -t && sudo systemctl reload sshd    # note: sshd, not ssh

RHEL 9 reads sshd_config.d/ drop-ins (the default config includes the directory). Test from a second terminal before disconnecting - the self-lockout classic respects no distro. Keys-only matters because the brute-force traffic hitting every public sshd becomes irrelevant when there’s nothing to guess.

3. firewalld: think in zones, close what you don’t serve

firewalld is already running - but check what the default zone actually permits:

sudo firewall-cmd --get-default-zone
sudo firewall-cmd --list-all           # cockpit and dhcpv6-client are often open
sudo firewall-cmd --permanent --remove-service=cockpit    # if unused
sudo firewall-cmd --permanent --add-service=https         # what you DO serve
sudo firewall-cmd --reload && sudo firewall-cmd --list-all

The default public zone frequently allows services you never asked for - Cockpit’s admin panel being the usual surprise. Treat the firewall as a statement of intent. Every closed port converts a surprise listener from incident into early warning.

4. Automatic security updates: dnf-automatic

sudo dnf install -y dnf-automatic
sudo sed -i 's/^upgrade_type.*/upgrade_type = security/;s/^apply_updates.*/apply_updates = yes/' /etc/dnf/automatic.conf
sudo systemctl enable --now dnf-automatic.timer

RHEL’s equivalent of unattended-upgrades, off by default. Set upgrade_type = security to limit updates to security errata. Check for pending reboots with dnf needs-restarting -r because kernel patches still require them.

5. SELinux: the control everyone disables first

The fix for an SELinux denial is a policy adjustment, not setenforce 0. SELinux enforcing is mandatory access control that contains a compromised service - the web server that gets popped can’t read what the policy never granted. It’s also the first thing half the internet’s troubleshooting advice tells you to turn off.

getenforce                          # must say Enforcing
sudo dnf install -y policycoreutils-python-utils setroubleshoot-server
sudo ausearch -m AVC -ts recent     # see what's actually being denied
# targeted fixes look like:
#   sudo setsebool -P httpd_can_network_connect 1
#   sudo semanage port -a -t http_port_t -p tcp 8080

If an app “needs SELinux off”, it needs a boolean, a port label, or a file context - all one-liners once ausearch shows the denial. A fleet where SELinux quietly became permissive during some 2 AM debugging session is a fleet with its best containment layer off; make getenforce part of what you monitor, not what you assume.

6. Kernel settings and auditd

The sysctl hardening block from the Ubuntu guide applies verbatim (drop it in /etc/sysctl.d/90-hardening.conf, then sudo sysctl --system). auditd is already installed and running on the RHEL family. Keep it that way and add watch rules for /etc/passwd, sudoers, and the SSH configs.

The drift problem wears a red hat too

RHEL-family boxes are bought for long uptimes, and long uptimes are where hardening quietly decays: the contractor for whom password auth got “temporarily” re-enabled, the zone that accumulated services, the enforcing mode that became permissive during an incident and never came back. Nobody re-runs a checklist on a server that isn’t complaining - which is how servers actually get owned.

The continuous half of the job is watching the controls themselves. AlertKick’s eBPF agent runs on the whole RHEL 9 family: SSH logins as they happen, changes to sshd_config, authorized_keys, and cron, new listening ports, processes from /tmp - streamed off-host, mapped to MITRE ATT&CK, and AI-triaged so patch-day noise stays quiet and genuine drift gets a page.

Start free, install the agent in about a minute, and let the checklist above be the state servers stay in - not just the state they passed through once.

Frequently asked questions

How do you harden a RHEL 9, AlmaLinux, or Rocky Linux server?
Create a named user in the wheel group, switch SSH to keys only with root login disabled via a drop-in in /etc/ssh/sshd_config.d/, review the firewalld default zone and remove services you do not serve, install and enable dnf-automatic for security updates, keep SELinux enforcing, apply a sysctl hardening block, and keep auditd running with watch rules for /etc/passwd, sudoers, and the SSH configs. The RHEL 9 family already ships with SELinux enforcing, firewalld running, and auditd on, so the job is mostly not dismantling the good defaults.
Should you disable SELinux to fix an application problem?
No. The fix for an SELinux denial is a policy adjustment, not setenforce 0. Run ausearch -m AVC -ts recent to see the actual denial, then apply a boolean with setsebool, a port label with semanage port, or a file context - all one-liners once you can see what is being denied. SELinux enforcing is the mandatory access control that contains a compromised service, and a fleet where it quietly became permissive has its best containment layer switched off.
How do you enable automatic security updates on RHEL 9?
Install dnf-automatic, set upgrade_type = security and apply_updates = yes in /etc/dnf/automatic.conf, and enable dnf-automatic.timer. It is the RHEL equivalent of unattended-upgrades and is off by default. Kernel patches still need reboots, so check for pending ones with dnf needs-restarting -r.
What is the sudo group on RHEL, AlmaLinux, and Rocky?
wheel, not sudo. Add the admin user with usermod -aG wheel, and verify sudo works as that user before making any SSH changes so you do not lock yourself out of the server.
What does firewalld allow by default on RHEL 9?
The default public zone frequently permits services you never asked for - Cockpit's admin panel and dhcpv6-client are the usual surprises. Run firewall-cmd --list-all to see what the zone actually permits, remove unused services with --permanent --remove-service, add only what the server serves, then reload and check the list again.
Why does a hardened RHEL server drift out of its hardened state over time?
RHEL-family servers are run for long uptimes, and hardening decays over that time: password auth re-enabled temporarily for a contractor, a firewall zone accumulating services, SELinux set permissive during an incident and never restored. Nobody re-runs a checklist on a server that is not complaining. Keeping the controls means monitoring them continuously - SSH logins, changes to sshd_config and authorized_keys, new listening ports, and getenforce status - rather than assuming they still hold.
rhel almalinux rocky linux hardening linux security