Tested on: AlmaLinux 9.4 (kernel 5.14.x). The same steps apply to Rocky Linux 9 and Red Hat Enterprise Linux 9.x — all three share the same package set, service names, and SELinux profile.

Why this matters

A fresh RHEL-family cloud image differs from Ubuntu’s defaults in two important ways:

  • SELinux is in enforcing mode by default — which is good, until an operator hits a denied syscall, panics, and runs setenforce 0. The baseline below assumes you’ll keep SELinux on and learn to tune it.
  • The default firewall is firewalld, not ufw — same idea, different vocabulary. Internal-API hosts often ship with firewalld enabled but permissive; never trust the defaults without inspecting.

Otherwise the threat model and the order of operations match the Ubuntu baseline: create a non-root user, harden SSH, enforce a host firewall, enable automatic security updates, turn on auditing, fix time. This guide is the RHEL-family-specific translation.

Order matters

Same warning as the Ubuntu baseline: do not enable the firewall before you have confirmed SSH access for your new non-root user.

1. First boot: create your operator account

Most RHEL-family cloud images ship with a cloud-user, ec2-user, or almalinux account. Treat that account as a bootstrap, not as a permanent operator identity.

 1sudo adduser alice
 2sudo usermod -aG wheel alice          # wheel = sudo equivalent on RHEL family
 3sudo mkdir -p /home/alice/.ssh
 4sudo chmod 700 /home/alice/.ssh
 5sudo tee /home/alice/.ssh/authorized_keys <<'EOF'
 6ssh-ed25519 AAAA...  alice@workstation 2026-05-16
 7EOF
 8sudo chmod 600 /home/alice/.ssh/authorized_keys
 9sudo chown -R alice:alice /home/alice/.ssh
10
11# Verify wheel grants sudo:
12grep -E '^\s*%wheel' /etc/sudoers   # should be present and uncommented
13
14# Test in a second terminal:
15#   ssh alice@host 'sudo -n true && echo ok'

2. Apply the SSH hardening baseline

The full guide is at /guides/ssh-hardening/; the drop-in file is identical on Ubuntu and RHEL. The only RHEL-specific detail: AllowGroups wheel instead of AllowGroups sudo.

 1sudo tee /etc/ssh/sshd_config.d/00-hardened.conf >/dev/null <<'EOF'
 2PasswordAuthentication no
 3PermitRootLogin        no
 4PubkeyAuthentication   yes
 5AllowGroups            wheel
 6MaxAuthTries           3
 7ClientAliveInterval    300
 8ClientAliveCountMax    2
 9EOF
10sudo sshd -t && sudo systemctl reload sshd

3. Enable the host firewall — firewalld

firewalld is zone-based; the public zone is the default on cloud images. For a typical web host:

 1sudo systemctl enable --now firewalld
 2
 3# Open only what this server needs (services are pre-defined):
 4sudo firewall-cmd --zone=public --permanent --add-service=ssh
 5sudo firewall-cmd --zone=public --permanent --add-service=http
 6sudo firewall-cmd --zone=public --permanent --add-service=https
 7
 8# Apply.
 9sudo firewall-cmd --reload
10sudo firewall-cmd --list-all --zone=public

For internal-API hosts, drop the default public zone services and use a custom zone bound to the private interface — see the firewalld zones guide for that pattern.

4. Automatic security updates — dnf-automatic

1sudo dnf install -y dnf-automatic

Configure /etc/dnf/automatic.conf:

 1[commands]
 2upgrade_type = security
 3random_sleep = 0
 4download_updates = yes
 5apply_updates = yes
 6
 7[emitters]
 8emit_via = stdio,motd
 9
10[email]
11email_from = root@$HOSTNAME
12email_to   = [email protected]

Enable the timer:

1sudo systemctl enable --now dnf-automatic.timer
2systemctl list-timers dnf-automatic.timer

Reboot policy on RHEL is a deliberate choice — dnf-automatic itself does not reboot. Either install dnf-utils and schedule needs-restarting -r checks, or use the needrestart package (EPEL) for advisories. For single-instance workloads, a scheduled weekly reboot via cron is the simplest approach.

5. Time sync — chrony

chrony is the RHEL family default. Verify it’s running:

1sudo systemctl enable --now chronyd
2chronyc tracking
3chronyc sources

Cloud images typically point at the provider’s NTP servers; that’s fine. For multi-cloud or on-prem fleets, point at a public stratum-2 pool plus internal NTP servers if you have them.

6. SELinux — verify enforcing, learn to tune

1sestatus
2# Expected output:
3# SELinux status:                 enabled
4# Current mode:                   enforcing
5# Policy MLS status:              enabled
6# Policy deny_unknown status:     allowed

If a service is being denied, the workflow is:

1# 1. Look at the AVC denial:
2sudo ausearch -m AVC -ts recent | tail
3
4# 2. Get a human-readable explanation:
5sudo ausearch -m AVC -ts recent | audit2why
6
7# 3. ONLY if the requested access is legitimate, generate a custom policy:
8sudo ausearch -m AVC -ts recent | audit2allow -M myapp-fix
9sudo semodule -i myapp-fix.pp

The temptation to set SELinux to permissive will arise. Don’t. SELinux is one of the few defences that has prevented real container-escape and file-disclosure vulnerabilities from being exploitable. Tune the policy; do not disable the enforcement.

7. Audit logging — auditd

auditd is installed by default on RHEL-family systems. The ruleset matches the Ubuntu baseline:

 1sudo tee /etc/audit/rules.d/baseline.rules <<'EOF'
 2# Identity / authentication
 3-w /etc/passwd  -p wa -k identity
 4-w /etc/shadow  -p wa -k identity
 5-w /etc/group   -p wa -k identity
 6-w /etc/sudoers -p wa -k identity
 7-w /etc/sudoers.d/ -p wa -k identity
 8
 9# SSH config
10-w /etc/ssh/sshd_config    -p wa -k sshd
11-w /etc/ssh/sshd_config.d/ -p wa -k sshd
12
13# SELinux config — useful for change detection
14-w /etc/selinux/config -p wa -k selinux
15
16# Privileged commands
17-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k privileged
18
19-e 2
20EOF
21
22sudo augenrules --load
23sudo systemctl restart auditd
24sudo auditctl -l

Forward auditd events off-host. RHEL has first-class support for audisp-remote which sends events to a central audit collector over TLS.

8. Remove or disable unused services

1# Listening sockets:
2sudo ss -ltnp
3
4# Enabled services:
5systemctl list-unit-files --state=enabled --type=service

Common candidates on a minimal install: cups-browsed, bluetooth, avahi-daemon. Do not remove firewalld, chronyd, auditd, sshd, or polkit.

9. Optional: brute-force log filtering

EPEL provides fail2ban; RHEL/AlmaLinux also bundles sshguard directly in the base repositories. Either is acceptable — the security boundary is key-only authentication, and these tools exist to keep your logs readable.

1sudo dnf install -y epel-release
2sudo dnf install -y fail2ban
3sudo systemctl enable --now fail2ban

10. Run a baseline scan

Two tools are worth running on a RHEL-family host:

1# CIS-style hardening guidance:
2sudo dnf install -y openscap-scanner scap-security-guide
3sudo oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_cis \
4    /usr/share/xml/scap/ssg/content/ssg-almalinux9-ds.xml | less
5
6# Lynis (also available via EPEL):
7sudo dnf install -y lynis
8sudo lynis audit system --quick

Investigate every FAIL from oscap and every WARNING from lynis. Document accepted deviations.

Gotchas

firewall-cmd without --permanent

firewall-cmd --add-service=... without --permanent makes a runtime change that disappears on the next reload or reboot. Always pair the change with a --reload, or use --permanent + --reload explicitly.

setenforce 0 is not a fix

Putting SELinux into permissive mode to “see if it works” is fine for a short diagnostic. Leaving it that way is a documented finding in any serious audit and undoes one of the strongest controls on the host.

dnf-automatic and kernel updates

Kernel updates apply, but the new kernel only runs after a reboot. Until then, your uname -r reports the old version while dnf shows the new package installed — confusing during compliance scans. Pair dnf-automatic with a documented reboot policy.

EPEL is a third-party repository

Tools mentioned above (fail2ban, lynis) come from EPEL. EPEL is maintained by Fedora contributors but is not Red Hat. Document this as a processor / supply-chain dependency where compliance demands a full SBOM.

Cloud-init replacing your changes on reboot

Some cloud images run cloud-init on every boot and can re-apply original network or user configuration. Check /etc/cloud/cloud.cfg and disable the modules whose output you do not want re-applied (commonly users-groups, set-passwords, ssh).

What this guide deliberately does not cover

  • Full CIS Benchmark profile compliance — the oscap step above is a start; full coverage is a separate guide.
  • Subscription Manager / Red Hat Insights — only relevant on upstream RHEL; AlmaLinux / Rocky operators can ignore.
  • podman / container host hardening — out of scope for this site.