Operating System Security Hardening for Linux Distributions.
A comprehensive, automated hardening script for Red Hat Enterprise Linux 8 and compatible derivatives.
Aligned with the CIS Red Hat Enterprise Linux 8 Benchmark v3.0.
| Distribution | Version |
|---|---|
| Red Hat Enterprise Linux | 8.x |
| CentOS Stream | 8 |
| AlmaLinux | 8.x |
| Rocky Linux | 8.x |
- Must be run as root (or via
sudo) - Active internet / yum repository access (for package removal)
authselect,update-crypto-policies,nmcliavailable (standard on RHEL 8)
sudo bash "RHEL8 Hardening Script"Audit artefacts (backups and findings) are written to /tmp/<hostname>_audit/.
Important: Reboot the host after the script completes to apply all kernel module, sysctl, and crypto-policy changes.
- Disables unused filesystems:
cramfs,freevxfs,jffs2,hfs,hfsplus,squashfs,vfat,udf - Disables unused network protocols:
dccp,sctp,rds,tipc - Disables USB storage (
usb-storage)
- Removes GCC compiler toolchain
- Removes legacy services:
rsh,ypserv,tftp,talk,telnet-server,xinetd - Removes LDAP server/client packages
- Removes
bind,vsftpd,dovecot,samba,squid,net-snmp
- Disables unnecessary services:
dhcpd,avahi-daemon,cups,nfslock,rpcgssd,rpcbind,rpcidmapd,rpcsvcgssd - Daemon umask set to
027
minlen = 14,dcredit = -1,ucredit = -1,ocredit = -1,lcredit = -1retry = 3,maxrepeat = 3- Configured via
/etc/security/pwquality.conf
- Enables and configures
auditdwith log rotation (max_log_file=50,num_logs=5) - Configures
journald: persistent storage, compression, syslog forwarding - Configures
rsyslogwith correctauth,authpriv.*facility rules and0640file permissions - Audit rules cover:
- Time changes, identity file modifications, network/locale changes
- Login/session events, permission and ownership changes
- Unsuccessful file access attempts, mount/unmount events, file deletions
- sudo/sudoers changes, kernel module operations, SELinux MAC policy
- Privileged command execution (
passwd,sudo,su,chage,newgrp,chsh) - Power/shutdown events via
systemctl
- Audit buffer set to 8192 (production-grade)
- Installs
cronie-anacron, enablescrond - Sets
root:rootownership and restrictive permissions (600/700) on all cron directories and files - Creates
/etc/at.allowand/etc/cron.allow; removes.denyfiles
- Disables
X11Forwarding,HostbasedAuthentication,PermitRootLogin,PermitEmptyPasswords,PermitUserEnvironment - Disables
AllowTcpForwarding,AllowAgentForwarding,GatewayPorts MaxAuthTries 4,ClientAliveInterval 300,ClientAliveCountMax 0,LoginGraceTime 60LogLevel VERBOSE,PrintLastLog yes,TCPKeepAlive no- Strong Ciphers:
chacha20-poly1305,aes256-gcm,aes128-gcm,aes256-ctr,aes192-ctr,aes128-ctr - Strong MACs (ETM preferred):
hmac-sha2-512-etm,hmac-sha2-256-etm,hmac-sha2-512,hmac-sha2-256 - Modern KexAlgorithms:
curve25519-sha256,ecdh-sha2-nistp521/384/256,diffie-hellman-group16/18-sha512 - Validates config with
sshd -tbefore restarting — prevents lockout - Sets
root:root 600on/etc/ssh/sshd_config - Configures login banner in
/etc/issue.netand/etc/motd
Applied via /etc/sysctl.d/99-CIS.conf and loaded immediately with sysctl --system:
| Parameter | Value | Purpose |
|---|---|---|
kernel.randomize_va_space |
2 |
Full ASLR |
kernel.kptr_restrict |
2 |
Hide kernel pointers |
kernel.dmesg_restrict |
1 |
Restrict dmesg to root |
kernel.yama.ptrace_scope |
1 |
Restrict ptrace |
kernel.perf_event_paranoid |
3 |
Restrict perf events |
kernel.sysrq |
0 |
Disable magic SysRq |
fs.suid_dumpable |
0 |
No core dumps from setuid |
fs.protected_hardlinks |
1 |
Protect hard links |
fs.protected_symlinks |
1 |
Protect symbolic links |
net.ipv4.tcp_syncookies |
1 |
SYN flood protection |
net.ipv4.conf.all.rp_filter |
1 |
Reverse path filtering |
net.ipv4.conf.all.log_martians |
1 |
Log martian packets |
net.ipv6.conf.all.disable_ipv6 |
1 |
Disable IPv6 |
- Disables IPv6 via sysctl,
/etc/sysconfig/network, and kernel module (modprobe.d/ipv6.conf)
- Disables all wireless interfaces via
nmcli radio all off
- Enables
with-faillockfeature viaauthselect
- Sets policy to
FUTUREviaupdate-crypto-policies
- Restricts
suto members of thewheelgroup viapam_wheel.so - Adds
roottowheel
- Default umask
027in/etc/bashrc,/etc/profile, and/etc/profile.d/CIS-umask.sh HISTSIZE=10000,HISTTIMEFORMAT,HISTCONTROL=ignoredups:ignorespace- Inactive account lock after 30 days (
useradd -D -f 30) /etc/login.defs:PASS_MAX_DAYS 90,PASS_MIN_DAYS 7,PASS_WARN_AGE 7,UID_MIN 1000
/etc/passwd 644,/etc/shadow 000,/etc/gshadow 000,/etc/group 644/boot/grub2/grub.cfg 600(BIOS) and/boot/efi/EFI/redhat/grub.cfg 600(EFI)/etc/rsyslog.conf 600- Sticky bit applied to all world-writable directories
The following findings are logged to $AUDITDIR for manual review — the script does not auto-remediate these:
- World-writable files
- Un-owned and un-grouped files
- SUID / SGID executables
- Empty password fields
- Home directory permissions and ownership
- Dot-file permissions
.netrc,.rhosts, and.forwardfile presence- Duplicate UIDs, GIDs, usernames, and group names
- Groups referenced in
/etc/passwdbut missing from/etc/group - Accounts with reserved UIDs that are not standard system accounts
- root PATH integrity (empty entries, trailing colons, group/other-writable dirs)
All backups and findings are written to /tmp/<hostname>_audit/.
| File | Contents |
|---|---|
sshd_config_<timestamp>.bak |
SSH config backup |
auditd.conf_<timestamp>.bak |
auditd config backup |
sysctl.conf_<timestamp>.bak |
sysctl backup |
service_remove_<timestamp>.log |
Package removal output |
audit_<timestamp>.log |
User/group integrity findings |
suid_exec_<timestamp>.log |
SUID executables found |
sgid_exec_<timestamp>.log |
SGID executables found |
world_writable_files_<timestamp>.log |
World-writable files |
home_permission_<timestamp>.log |
Home directory permission issues |
A comprehensive, automated hardening script for Red Hat Enterprise Linux 9 and compatible derivatives.
Aligned with the CIS Red Hat Enterprise Linux 9 Benchmark v2.0.0.
| Distribution | Version |
|---|---|
| Red Hat Enterprise Linux | 9.x |
| AlmaLinux | 9.x |
| Rocky Linux | 9.x |
- Must be run as root (or via
sudo) - Active internet / dnf repository access (for package installation and removal)
authselect,update-crypto-policies,nmcli,nftavailable (standard on RHEL 9)
sudo bash "RHEL9 Hardening Script"Audit artefacts (backups and findings) are written to /tmp/<hostname>_audit/.
Important: Reboot the host after the script completes to apply all kernel module, sysctl, crypto-policy, and IPv6 disable changes.
- Disables unused filesystems:
cramfs,freevxfs,jffs2,hfs,hfsplus,squashfs,udf - Note:
vfatis intentionally left enabled — RHEL 9 requires it for the EFI System Partition - Disables unused network protocols:
dccp,sctp,rds,tipc - Disables USB storage (
usb-storage) viamodprobe.d
- Removes GCC/make/autoconf compiler toolchain
- Removes legacy services:
rsh,ypserv,tftp,talk,telnet-server,xinetd - Removes LDAP server/client packages
- Removes
bind,vsftpd,dovecot,samba,squid,net-snmp,sendmail,postfix,xorg-x11-server - Each package removal is tolerant of packages already absent — script does not abort
Installs the following security packages if not already present:
| Package | CIS Control | Purpose |
|---|---|---|
aide |
CIS 1.3.1 | Host-based file integrity monitoring |
fapolicyd |
CIS 1.7 | Application allowlisting |
usbguard |
CIS 1.1.1.8 | USB device policy enforcement |
dnf-automatic |
CIS 1.9 | Automatic security-only updates |
nftables |
CIS 3.5 | Stateful packet filtering firewall |
audit |
CIS 4.1 | Kernel audit daemon |
rsyslog |
CIS 4.2 | System event logging |
chrony |
CIS 2.1.2 | NTP time synchronisation |
libpwquality |
CIS 5.4 | Password complexity enforcement |
gpgcheck=1andlocalpkg_gpgcheck=1enforced in/etc/dnf/dnf.confclean_requirements_on_remove=trueto avoid orphaned packages- All
.repofiles scanned andgpgcheck=0entries corrected togpgcheck=1
dnf-automaticconfigured for security-only updatesapply_updates=yes— patches applied automatically- Timer unit enabled:
dnf-automatic.timer
- Disables unnecessary services:
dhcpd,avahi-daemon,cups,nfslock,rpcgssd,rpcbind,rpcidmapd,rpcsvcgssd,bluetooth,autofs,nfs-server,nis,kdump - Daemon umask set to
027
cmddeny all— restricts chrony management queriescmdallow 127.0.0.1— permits local management only- chrony daemon runs as the unprivileged
chronyuser
- Written to drop-in
/etc/security/pwquality.conf.d/CIS.conf(RHEL 9 native approach) minlen=14,dcredit=-1,ucredit=-1,ocredit=-1,lcredit=-1retry=3,maxrepeat=3,difok=8,gecoscheck=1
- Configured via
/etc/security/faillock.conf:deny=5,unlock_time=900,fail_interval=900 - Applied via
authselect select sssd --force+enable-feature with-faillock
- Persistent storage, compression enabled, forwarding to syslog
* hard core 0set in/etc/security/limits.confStorage=noneandProcessSizeMax=0set in/etc/systemd/coredump.conf- Combined with
fs.suid_dumpable=0via sysctl
auth,authpriv.*→/var/log/securekern.*,daemon.*,syslog.*→/var/log/messages*.emerg→ broadcast to all logged-in users- File create mode set to
0640
max_log_file=50,num_logs=5, log rotation set tokeep_logsspace_left_action=email,admin_space_left_action=haltlog_format=ENRICHED— structured enriched event format (new in RHEL 9)- Audit rules cover:
- Time changes, identity file modifications, network/locale changes
- Login/session events including
/run/faillock/(RHEL 9 faillock directory) - Permission and ownership changes, unsuccessful file access
- Mount events, file deletions
- sudo/sudoers and
/etc/sudoers.d/changes - Kernel module operations including
finit_modulesyscall - SELinux MAC policy and
/usr/share/selinux/ - Extended privileged command set:
passwd,sudo,su,chage,newgrp,chsh,chfn,gpasswd,usermod,useradd,userdel - Power/shutdown via
systemctl - fapolicyd policy directory
/etc/fapolicyd/
- Audit buffer: 8192; failure mode: 2 (panic on loss)
- Installs
cronie, enablescrond root:rootownership and600/700permissions on all cron files and directories/etc/at.allowand/etc/cron.allowcreated;.denyfiles removed
- Legal warning written to
/etc/issue.netand/etc/motd /etc/issuesymlinked to/etc/issue.net- Banner path referenced in SSH drop-in config
RHEL 9 uses the drop-in directory approach. All settings are written to /etc/ssh/sshd_config.d/50-CIS-hardening.conf rather than editing the main sshd_config directly.
Authentication & session controls:
| Setting | Value |
|---|---|
PermitRootLogin |
no |
PermitEmptyPasswords |
no |
PermitUserEnvironment |
no |
HostbasedAuthentication |
no |
IgnoreRhosts |
yes |
MaxAuthTries |
4 |
LoginGraceTime |
60 |
ClientAliveInterval |
300 |
ClientAliveCountMax |
0 |
MaxStartups |
10:30:60 |
TCPKeepAlive |
no |
AllowTcpForwarding |
no |
AllowAgentForwarding |
no |
GatewayPorts |
no |
X11Forwarding |
no |
LogLevel |
VERBOSE |
SyslogFacility |
AUTHPRIV |
Cryptographic settings:
| Setting | Algorithms |
|---|---|
Ciphers |
chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-gcm@openssh.com, aes256-ctr, aes192-ctr, aes128-ctr |
MACs |
hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512, hmac-sha2-256 |
KexAlgorithms |
curve25519-sha256, curve25519-sha256@libssh.org, diffie-hellman-group16/18-sha512, ecdh-sha2-nistp521/384/256 |
- Config validated with
sshd -tbefore restart — prevents SSH lockout
Applied via /etc/sysctl.d/99-CIS.conf:
| Parameter | Value | Purpose |
|---|---|---|
kernel.randomize_va_space |
2 |
Full ASLR |
kernel.kptr_restrict |
2 |
Hide kernel symbol addresses |
kernel.dmesg_restrict |
1 |
Restrict dmesg to root |
kernel.yama.ptrace_scope |
2 |
Restrict ptrace to parent only |
kernel.perf_event_paranoid |
3 |
Restrict perf events |
kernel.sysrq |
0 |
Disable magic SysRq |
fs.suid_dumpable |
0 |
No core dumps from setuid |
fs.protected_hardlinks |
1 |
Protect hard links |
fs.protected_symlinks |
1 |
Protect symbolic links |
fs.protected_fifos |
2 |
Protect FIFOs (RHEL 9) |
fs.protected_regular |
2 |
Protect regular files (RHEL 9) |
user.max_user_namespaces |
0 |
Disable unprivileged user namespaces |
net.ipv4.tcp_syncookies |
1 |
SYN flood protection |
net.ipv4.conf.all.rp_filter |
1 |
Reverse path filtering |
net.ipv4.conf.all.log_martians |
1 |
Log martian packets |
net.ipv6.conf.all.disable_ipv6 |
1 |
Disable IPv6 |
Container hosts: Review
user.max_user_namespaces=0— container runtimes (Podman, Docker) may require a value greater than0.
- Disabled via sysctl (
net.ipv6.conf.*.disable_ipv6=1) options ipv6 disable=1written to/etc/modprobe.d/ipv6.conf
nftablesinstalled and enabled;firewalld,iptables,ip6tablesdisabled to prevent conflicts- Default-deny stateful ruleset written to
/etc/nftables/main.nft:- Loopback traffic accepted
- Invalid packets dropped
- Established/related connections accepted
- ICMP/ICMPv6 rate-limited (10/second)
- SSH (port 22) accepted for new connections
- All other inbound traffic logged and dropped
- Outbound traffic accepted by default
Review required: Open only the ports your workload actually needs. Edit
/etc/nftables/main.nftbefore production deployment.
- All wireless interfaces disabled via
nmcli radio all off
- System-wide policy set to
FUTUREviaupdate-crypto-policies - RHEL 9
DEFAULTpolicy already disables SHA-1;FUTUREadditionally removes DH groups below 3072-bit and further restricts TLS
pam_wheel.so use_uidenforced in/etc/pam.d/su(idempotent — removes duplicates before inserting)rootadded towheelgroup
Defaults requiretty— sudo requires a real terminalDefaults logfile="/var/log/sudo.log"— all sudo commands loggedDefaults timestamp_timeout=0— password required for every sudo invocation (no cached token)
- Default umask
027in/etc/bashrc,/etc/profile, and/etc/profile.d/CIS-umask.sh HISTSIZE=10000,HISTTIMEFORMAT,HISTCONTROL=ignoredups:ignorespace- Inactive account lock after 30 days (
useradd -D -f 30) /etc/login.defs:PASS_MAX_DAYS 90,PASS_MIN_DAYS 7,PASS_WARN_AGE 7,UID_MIN 1000ENCRYPT_METHOD SHA512,SHA_CRYPT_MIN_ROUNDS 5000
/etc/passwd 644,/etc/shadow 000,/etc/gshadow 000,/etc/group 644- BIOS:
/boot/grub2/grub.cfg 600 - EFI:
/boot/efi/EFI/redhat/grub.cfg,/boot/efi/EFI/almalinux/grub.cfg,/boot/efi/EFI/rocky/grub.cfgall600 /etc/rsyslog.conf 600- Sticky bit applied to all world-writable directories
- AIDE database initialised at
/var/lib/aide/aide.db.gz - Daily integrity check scheduled via
/etc/cron.daily/aide - Check results emailed to
root
- Trust database regenerated with
fagenrules --load fapolicydservice enabled and started- Policy changes audited via audit rule on
/etc/fapolicyd/
- Policy auto-generated from currently connected devices (
usbguard generate-policy) - Policy written to
/etc/usbguard/rules.confwith600permissions usbguardservice enabled and started
Findings written to $AUDITDIR for manual review:
- World-writable files
- Un-owned and un-grouped files
- SUID / SGID executables
- Empty password fields
- Home directory existence and ownership (UID ≥ 1000)
- Dot-file permissions (group/other-writable)
.netrc,.rhosts,.forwardfile presence- Duplicate UIDs, GIDs, usernames, and group names
- Groups referenced in
/etc/passwdbut missing from/etc/group - NIS
+:entries in/etc/passwd,/etc/shadow,/etc/group - UID-0 accounts other than root
All backups and findings are written to /tmp/<hostname>_audit/.
| File | Contents |
|---|---|
sshd_config_<timestamp>.bak |
Main SSH config backup |
sshd_drop_<timestamp>.bak |
SSH drop-in backup |
auditd.conf_<timestamp>.bak |
auditd config backup |
faillock.conf_<timestamp>.bak |
faillock config backup |
sysctl.conf_<timestamp>.bak |
sysctl backup |
dnf.conf_<timestamp>.bak |
DNF config backup |
chrony.conf_<timestamp>.bak |
chrony config backup |
nftables_<timestamp>.bak |
nftables ruleset backup |
pkg_remove_<timestamp>.log |
Package removal output |
pkg_install_<timestamp>.log |
Package installation output |
aide_init_<timestamp>.log |
AIDE database initialisation log |
svc_restart_<timestamp>.log |
Service restart output |
user_group_audit_<timestamp>.log |
User/group integrity findings |
suid_exec_<timestamp>.log |
SUID executables found |
sgid_exec_<timestamp>.log |
SGID executables found |
world_writable_files_<timestamp>.log |
World-writable files |
The script prints these reminders on completion:
- Replace
YOUR_COMPANY_NAMEin/etc/issue.netand/etc/motd - Review
/etc/nftables/main.nft— open only ports required by this server's workload - If this is a container host, review
user.max_user_namespaces=0in/etc/sysctl.d/99-CIS.conf - Verify
fapolicydpolicy does not block legitimate application workloads - Reboot to apply kernel module, sysctl, crypto-policy, and IPv6 disable changes
| Area | RHEL 8 Script | RHEL 9 Script |
|---|---|---|
| CIS Benchmark | v3.0.0 | v2.0.0 |
| Package manager | yum |
dnf |
| SSH config method | Edit /etc/ssh/sshd_config directly |
Drop-in /etc/ssh/sshd_config.d/50-CIS-hardening.conf |
| Password quality config | /etc/security/pwquality.conf |
Drop-in /etc/security/pwquality.conf.d/CIS.conf |
| Faillock config | authselect only |
authselect + /etc/security/faillock.conf |
| Firewall | Services disabled | Active nftables default-deny ruleset |
| Application allowlisting | Not present | fapolicyd enabled |
| File integrity | Not present | AIDE initialised + daily cron check |
| USB control | Module blacklist only | USBGuard policy + service |
| Automatic updates | Not present | dnf-automatic security-only updates |
| NTP | Not configured | chrony hardened |
| Audit log format | Default | ENRICHED (structured) |
vfat module |
Disabled | Left enabled (required for EFI) |
fs.protected_fifos |
Not set | 2 |
fs.protected_regular |
Not set | 2 |
user.max_user_namespaces |
Not set | 0 (disable unprivileged namespaces) |
kernel.yama.ptrace_scope |
1 |
2 (stricter) |
| sudo hardening | Not configured | requiretty, logfile, timestamp_timeout=0 |
| login.defs | Basic password aging | + ENCRYPT_METHOD SHA512, SHA_CRYPT_MIN_ROUNDS 5000 |
| Audit: finit_module | Not audited | Audited |
| Audit: faillock dir | Not audited | /run/faillock/ audited |
| Audit: fapolicyd dir | Not present | /etc/fapolicyd/ audited |
These scripts make system-wide changes including service removal, SSH restart, kernel parameter modification, and firewall rule deployment. Before running in production:
- Test in a non-production or staging environment first
- Ensure out-of-band console access (IPMI/iDRAC/iLO) before running — SSH will be restarted
- Review environment-specific settings:
vfatdisable (RHEL 8) may break EFI-only systems- nftables rules (RHEL 9) must be adapted to open the ports your workload requires
user.max_user_namespaces=0(RHEL 9) will break Podman/Docker on container hostsfapolicyd(RHEL 9) may block custom or third-party application binaries
- Replace
YOUR_COMPANY_NAMEin/etc/issue.netand/etc/motdbefore deploying - Reboot after the script completes to activate all changes