Securing and optimizing a RHEL 9 or RHEL 10 server
The old book "Securing and Optimizing Linux: Red Hat Edition" was written for Red Hat Linux 6 and told you to recompile half the system. Today Red Hat Enterprise Linux (RHEL) and its rebuilds ship with SELinux, a firewall, system-wide crypto policies and tuning profiles already in place. Your job is to keep them on, remove what you do not need, and set a few things deliberately. This guide does that for RHEL 9 and 10, AlmaLinux and Rocky Linux, in the order you would do it on a new server.
- May 2032end of RHEL 9 maintenance support
- May 2035end of RHEL 10 maintenance support
- 6.12kernel series in RHEL 10
- 10 yearsstandard life cycle per major release
Pick the release
RHEL 9 came out on May 18, 2022. Full support ends May 31, 2027 and maintenance support (security fixes) May 31, 2032. RHEL 10 came out on May 20, 2025, with full support until May 31, 2030 and maintenance until May 31, 2035. Paid add-ons extend both further. These are Red Hat's published dates as of November 2026; Red Hat calls future dates approximate. For a new server, choose RHEL 10 unless an application is only certified on 9.
AlmaLinux and Rocky Linux rebuild RHEL from its sources and use the same commands. The main differences: they do not use subscription-manager, and Red Hat services such as its kernel live patches need a RHEL subscription. Check your release with cat /etc/os-release.
Things that changed in RHEL 10 and matter for hardening:
- OpenSSH 9.9. DSA keys are no longer supported, and SSH host keys are now mode 0600 instead of 0640.
- From RHEL 10.1, the default crypto policy enables post-quantum key exchange (ML-KEM).
- FIPS mode can only be enabled at installation time (
fips=1on the installer's kernel command line). Switching an installed system to FIPS mode is not supported. - The audit dispatcher (audisp) is now part of auditd.
- Custom TuneD profiles moved from
/etc/tuned/to/etc/tuned/profiles/. - The installer gives newly created users administrator rights by default. Untick that for accounts that should not have sudo.
Install small
Choose the Minimal Install environment in the installer and add only what the server needs. Fewer packages mean fewer updates and fewer things that can be attacked. If you install many servers, put the choices in a Kickstart file (see the KickStart HOWTO). Consider separate file systems for /var, /var/log and /var/log/audit, so a flood of logs cannot fill the root file system, and LUKS encryption for laptops and servers outside a locked room.
After the first boot, look at what runs and what is enabled, and remove the rest:
sudo ss -tulpn
systemctl list-unit-files --state=enabled
sudo systemctl disable --now NAME.service # a service you do not need
sudo dnf remove PACKAGE # and its package
Register and update
sudo subscription-manager register # RHEL only
sudo dnf upgrade
sudo dnf updateinfo list --updates --security # security advisories not yet installed
sudo dnf update --security # install only security fixes
sudo dnf needs-restarting -r # tells you if a reboot is needed
Look at one advisory with dnf updateinfo info RHSA-2026:1234 (use a real ID from the list) and install just that one with dnf update --advisory=RHSA-2026:1234.
Automatic security updates
sudo dnf install dnf-automatic
sudoedit /etc/dnf/automatic.conf
In the [commands] section set:
upgrade_type = security
reboot = when-needed
Then enable the timer that downloads and installs:
sudo systemctl enable --now dnf-automatic-install.timer
systemctl list-timers dnf-automatic*
reboot = when-needed restarts after kernel or systemd updates, with a five-minute warning by default. If unplanned reboots are not acceptable, leave it at never and schedule a weekly maintenance reboot instead.
Kernel live patching
With a RHEL subscription, kpatch applies critical kernel fixes without a reboot. On RHEL 10 it is supported from 10.2:
sudo dnf install kpatch-dnf
sudo dnf kpatch auto # subscribe current and future kernels
sudo kpatch list
Live patches cover a limited time per kernel. You still need regular reboots into new kernels.
SELinux: keep it enforcing
SELinux confines each service to the files, ports and actions its policy allows. A broken web application then cannot read /etc/shadow or open a shell to the attacker's server. RHEL runs it in enforcing mode by default.
getenforce # Enforcing
sestatus # mode, policy, and the mode set in /etc/selinux/config
When an application is blocked, fix the specific cause. Most problems are one of three kinds:
- Files in an unusual place
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?" sudo restorecon -Rv /srv/www- A service on a non-standard port
sudo semanage port -l | grep ssh_port_t sudo semanage port -a -t ssh_port_t -p tcp 2222- A documented on/off switch (boolean)
getsebool -a | grep httpd sudo setsebool -P httpd_can_network_connect_db on
To see what was denied and why:
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR,USER_SELINUX_ERR -ts recent
sudo dnf install setroubleshoot-server
sudo sealert -l "*"
While debugging, make one domain permissive instead of the whole system: sudo semanage permissive -a httpd_t, and remove it afterwards with -d. Do not reach for audit2allow first. It writes a rule that allows whatever was denied, including things that should stay denied.
Do not disable SELinux. Turning it off (or setting the whole system permissive) removes the protection for every service at once, and Red Hat strongly discourages it. Fix the label, port or boolean, or make the one affected domain permissive for a short time while you investigate, then remove that exception.
firewalld: open only what you serve
firewalld is on by default, uses nftables underneath, and puts interfaces in the public zone. On a typical fresh install that zone allows ssh, dhcpv6-client and cockpit; check yours with --list-all.
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --remove-service=cockpit
sudo firewall-cmd --reload
To allow SSH only from your admin network, replace the open service with a rich rule:
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.0/24" service name="ssh" accept'
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --reload
Do this from a console or from inside that network, and check you can still log in before closing your session. The Firewall HOWTO has more on zones and logging.
SSH
In RHEL 10, PermitRootLogin defaults to prohibit-password: root may log in with a key but not a password. Go further. Set up key login for your own account first (ssh-keygen -t ed25519, then ssh-copy-id; in FIPS mode use -t ecdsa, because Ed25519 is not FIPS-approved). Then add a drop-in file. sshd uses the first value it reads, and RHEL's own settings are in 50-redhat.conf, so pick a lower number:
# /etc/ssh/sshd_config.d/10-local.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowGroups wheel
Keep your current session open. Before you reload or restart sshd, check that your own account is in wheel (id) because of AllowGroups wheel, leave the session you are working in open (and ideally a second one), and have console access ready. Existing sessions survive a reload; log in from a new terminal to test, and close the old session only when that works.
sudo sshd -t && sudo systemctl reload sshd
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication'
If you move SSH to another port, label the port for SELinux (above) and open it in firewalld, or sshd will fail to start or be unreachable.
Crypto policies
One setting controls which protocols and algorithms OpenSSL, GnuTLS, NSS, OpenSSH, Libreswan and other libraries accept:
update-crypto-policies --show # DEFAULT
sudo update-crypto-policies --set FUTURE # stricter; then reboot
DEFAULT is a sound choice for most servers. FUTURE rejects more older algorithms and may break connections to old clients. Avoid LEGACY except as a short-term fix for one old system, and prefer a narrow custom sub-policy in /etc/crypto-policies/policies/modules/ over weakening the whole machine. Setting the FIPS policy alone does not put the system in FIPS mode.
Auditing with auditd
The audit daemon records security events (logins, sudo use, changes to watched files) in /var/log/audit/audit.log. Red Hat ships ready-made rule sets in /usr/share/audit/sample-rules/. A common starting point:
cd /usr/share/audit/sample-rules
sudo cp 10-base-config.rules 30-stig.rules 31-privileged.rules 99-finalize.rules /etc/audit/rules.d/
sudo augenrules --load
sudo auditctl -l | head
Read each file before you copy it; the sample rules are pieces of a policy, not meant to be used all at once. Use service auditd restart rather than systemctl for auditd: Red Hat documents service as the correct way, with systemctl only for enable and status. Search and report:
sudo ausearch -m USER_LOGIN -ts recent -sv no # failed logins
sudo aureport --login -i
File integrity with AIDE
sudo dnf install aide
sudo aide --init
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
sudo aide --check
Red Hat recommends checking at least weekly and ideally daily, for example with this line in /etc/crontab: 05 4 * * * root /usr/sbin/aide --check. After planned updates, run sudo aide --update, review the report, and rename the new database the same way. Keep a copy of the database on another system.
For a formal baseline (CIS, DISA STIG, PCI DSS), RHEL's OpenSCAP tools and the scap-security-guide content can scan the system and generate fixes; see the Security hardening guide below.
Optimizing: TuneD first
The old advice was to rebuild packages with special compiler flags. Do not. Red Hat's packages are already built well, and rebuilt ones lose support and security updates. Tune the running system instead, and measure before and after.
TuneD applies a profile of kernel, disk, CPU and network settings. The installer picks one automatically (for example virtual-guest for a VM):
tuned-adm active
tuned-adm recommend
sudo tuned-adm profile throughput-performance
tuned-adm verify
| Profile | Use it for |
|---|---|
balanced | general use; trades some speed for lower power |
throughput-performance | physical servers: file, web, build hosts |
latency-performance / network-latency | low-latency services; disables power saving |
network-throughput | bulk network transfers; bigger network buffers |
virtual-guest / virtual-host | VMs and KVM hosts |
powersave | maximum power saving |
To change one setting, make a child profile instead of editing system files. On RHEL 10 it goes in /etc/tuned/profiles/myserver/tuned.conf (on RHEL 9, /etc/tuned/myserver/tuned.conf):
[main]
include=throughput-performance
[sysctl]
vm.swappiness=5
Activate it with sudo tuned-adm profile myserver. Other quick wins: give databases enough RAM rather than more swap, put write-heavy data on SSDs, and look at top, vmstat 5 and iostat -x 5 (package sysstat) to find the real bottleneck before changing anything.
Checklist
- Supported release; minimal install; unneeded services removed.
- Security updates automatic; reboots planned; kpatch if subscribed.
- SELinux enforcing; no permissive domains left over.
- firewalld: only needed services; SSH limited to admin networks.
- SSH: keys only, no root login, checked with
sshd -T. - Crypto policy
DEFAULTorFUTURE, notLEGACY. - auditd rules loaded; AIDE database built and copied off the host.
- TuneD profile chosen and verified.
- Backups running and a restore tested (see the Security HOWTO).
The original 2001 book (version 1.3) is kept by the LDP at tldp.org. It describes Red Hat Linux 6.x and is of historical interest only.
Sources
- Red Hat Enterprise Linux life cycle, Red Hat Customer Portal (dates from its life-cycle data)
- RHEL 10.0 release notes, Red Hat
- Security hardening (crypto policies, FIPS, AIDE, OpenSCAP), RHEL 10 documentation
- Managing and monitoring security updates, RHEL 10 documentation
- Auditing the system, RHEL 10 documentation
- Managing software with the DNF tool (dnf-automatic), RHEL 10 documentation
- DNF Automatic (upgrade_type, reboot), DNF documentation
- needs-restarting, dnf-plugins-core documentation
- Applying patches with kernel live patching, RHEL 10 documentation
- Using SELinux, RHEL 10 documentation
- semanage-port(8), man7.org
- Configuring firewalls and packet filters, RHEL 10 documentation
- firewalld.richlanguage(5) and firewall-cmd(1), firewalld.org
- Securing networks (OpenSSH), RHEL 10 documentation
- sshd_config(5), man7.org
- auditctl(8) and augenrules(8), man7.org
- Monitoring and managing system status and performance (TuneD), RHEL 10 documentation
- Customizing TuneD profiles, RHEL 9 documentation