KB
Security & Hardening

Hardening Linux Server

6 min read1192 words27 code blocks

At a glance#

  • Purpose: Baseline security hardening checklist for a RHEL-family Linux server.
  • Applies to: RHEL, CentOS, Rocky and AlmaLinux 7+. For Ubuntu, see the related article below.
  • Risk: Medium — several steps can lock you out if applied carelessly.
  • Time: 1–2 hours for a full pass.

Overview#

This is a baseline checklist to apply to a RHEL-family server before it carries production traffic. It is deliberately ordered so that the changes most likely to lock you out come after the ones that will not.

Ubuntu servers have their own, more detailed article: Ubuntu Server Hardening Documentation. Use that one for Ubuntu hosts rather than translating this.

Warning: Keep a second SSH session open throughout, and have console access available. Several steps below — firewall rules and SSH changes in particular — can lock you out of a remote machine. Never close your only session until you have proved the new configuration works in a new one.

Before you start#

  • Root or sudo access.
  • Console access (vCenter, iDRAC/iLO) as a fallback.
  • Agreement on a maintenance window if the host is live.
  • A record of what the server is for, so you know which services are legitimately needed.

Procedure#

1. Patch the system#

The single highest-value hardening step. Most breaches exploit known, patched vulnerabilities.

bash
sudo yum update -y            # RHEL 7
sudo dnf update -y            # RHEL 8/9

Reboot if the kernel was updated:

bash
sudo needs-restarting -r || sudo reboot

Establish a recurring patch schedule. A one-off update is not hardening.

2. Remove unnecessary packages and disable unused services#

Every listening service is attack surface. List what is actually running:

bash
sudo systemctl list-units --type=service --state=running
sudo ss -tulpn

For each listening port, decide whether it is needed. Disable what is not:

bash
sudo systemctl disable --now <service>

Common candidates on a server build: cups, avahi-daemon, bluetooth, rpcbind, postfix (unless the host actually sends mail).

3. Enforce a password policy#

bash
sudo nano /etc/security/pwquality.conf
text
minlen = 14
dcredit = -1
ucredit = -1
ocredit = -1
lcredit = -1

Set password ageing defaults in /etc/login.defs:

text
PASS_MAX_DAYS   90
PASS_MIN_DAYS   1
PASS_WARN_AGE   14

4. Restrict administrative access#

Grant sudo only to accounts that need it, and prefer group membership over individual entries:

bash
sudo usermod -aG wheel username

Review who currently holds it:

bash
grep -Po '^sudo.+:\K.*$' /etc/group
lid -g wheel

Remove or lock any account that is no longer needed:

bash
sudo usermod -L olduser

5. Harden SSH#

Edit the daemon configuration:

bash
sudo nano /etc/ssh/sshd_config
text
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
Protocol 2
Warning: Before setting PasswordAuthentication no, confirm your SSH key works. Log in with the key from a new session first. Getting this wrong on a remote server means a trip to the console.

Validate the syntax before restarting — this catches typos that would otherwise stop sshd from starting at all:

bash
sudo sshd -t
sudo systemctl restart sshd

6. Configure the firewall#

Default-deny, then allow only what is required:

bash
sudo systemctl enable --now firewalld
sudo firewall-cmd --set-default-zone=drop
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Review the result:

bash
sudo firewall-cmd --list-all
Warning: Adding SSH to the permanent rules before reloading is essential. Reloading with a drop-all zone and no SSH rule ends the session immediately.

7. Protect the boot loader#

Prevents the single-user-mode password reset described in Changing root password Redhat.

bash
sudo grub2-setpassword
sudo grub2-mkconfig -o /boot/grub2/grub.cfg

Record the password in the team password manager. Losing it makes recovery considerably harder.

8. Tune kernel network parameters#

bash
sudo nano /etc/sysctl.d/99-hardening.conf
text
# Ignore ICMP broadcasts
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Do not accept source-routed packets
net.ipv4.conf.all.accept_source_route = 0

# Do not accept ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0

# Log packets with impossible addresses
net.ipv4.conf.all.log_martians = 1

# Enable SYN flood protection
net.ipv4.tcp_syncookies = 1

# Disable IPv6 if genuinely unused on this network
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1

Apply:

bash
sudo sysctl --system
Note: Only disable IPv6 if you are certain nothing on the host depends on it. Some applications bind to ::1 and will fail to start without it.

9. Enable and configure auditing#

The audit daemon records who touched what, which is what turns an incident into an investigable one.

bash
sudo systemctl enable --now auditd

Add persistent watches on files that matter. Put them in a rules file rather than using auditctl directly, so they survive reboot:

bash
sudo nano /etc/audit/rules.d/hardening.rules
text
-w /etc/passwd -p wa -k passwd-changes
-w /etc/shadow -p wa -k shadow-changes
-w /etc/sudoers -p wa -k sudoers-changes
-w /var/www/html -p wa -k web-content

Load them:

bash
sudo augenrules --load
sudo auditctl -l

Query the audit log by key:

bash
sudo ausearch -k passwd-changes -i
sudo ausearch -ts today -k sudoers-changes -i
sudo aureport --summary
Note: The original version of this article used auditctl -w directly. Those rules are lost on reboot. Use the rules file above instead.

10. Keep SELinux enforcing#

bash
getenforce

If it returns anything other than Enforcing, fix it in /etc/selinux/config and reboot. Disabling SELinux to make an application work is a shortcut that costs you a major security control — write a policy exception instead.

11. Verify logging and backups#

Confirm logs are being written and shipped somewhere off-host, so they survive a compromise of this machine:

bash
sudo systemctl status rsyslog
sudo journalctl --disk-usage

Confirm the host is covered by the backup schedule, and that a restore has actually been tested.

Verification#

Work through this list after applying the changes:

bash
# SSH config valid and root login refused
sudo sshd -t
ssh root@localhost              # should be rejected

# Firewall active and minimal
sudo firewall-cmd --list-all

# SELinux enforcing
getenforce

# Audit rules loaded
sudo auditctl -l

# Nothing unexpected listening
sudo ss -tulpn

# Kernel parameters applied
sudo sysctl net.ipv4.tcp_syncookies

Most importantly, open a completely new SSH session and confirm you can still log in and escalate to root before closing your existing one.

Troubleshooting#

SymptomCause and fix
Locked out after firewall changeConnect via console and run sudo firewall-cmd --add-service=ssh --permanent && sudo firewall-cmd --reload.
sshd will not startA syntax error in sshd_config. Run sudo sshd -t to see the offending line.
Application breaks after hardeningUsually SELinux or the firewall. Check sudo ausearch -m avc -ts recent for SELinux denials before disabling anything.
Audit rules gone after rebootThey were added with auditctl rather than a rules file. Move them to /etc/audit/rules.d/.
Cannot edit GRUB at bootExpected — that is what step 7 does. Use the GRUB password from the password manager.