Introduction
A default Linux server installation exposes a far larger attack surface than most realize: unnecessary RPM packages, open network services, permissive SSH configurations, and inactive audit systems. Security hardening is not a one-time operation but a full lifecycle checklist from installation to runtime. This article follows the CIS Benchmark framework to organize hardening essentials, with directly executable configurations for each step.
System Minimal Installation Principles
Package Management Trimming
The core principle of minimal installation: install only what you need, not what you might need.
# Select Minimal Install during installation (RHEL family) or minimal image (Debian family)
# After installation, audit installed package groups and remove unneeded ones
yum grouplist # RHEL/CentOS
dnf group list # Fedora/RHEL 8+
# Remove unneeded package groups
dnf group remove "GNOME Desktop" "Graphical Administration Tools"
# Audit installed packages, sorted by size
rpm -qa --queryformat '%{SIZE} %{NAME}\n' | sort -rn | head -30
# Remove known unnecessary packages
dnf remove -y ypbind rsh-server ypserv telnet-server talk-server
Debian/Ubuntu family:
# View manually installed packages (candidates for safe removal)
apt-mark showmanual
# Remove unnecessary service packages
apt purge -y rsh-client rsh-redone-server telnetd tftpd xinetd
Service Trimming
After installation, audit running services and disable all non-essential ones:
# List all enabled services
systemctl list-unit-files --type=service --state=enabled
# Audit and disable unnecessary services one by one
systemctl disable --now avahi-daemon cups bluetooth nfs-server 2>/dev/null
# While disabling unneeded services, ensure critical services are not accidentally stopped
# Common candidates for disabling: avahi-daemon cups bluetooth rpcbind nfs-server
# Handle with care: NetworkManager firewalld sshd
A guiding principle: if you are unsure whether a service is needed, check its description and dependencies:
systemctl cat rpcbind.service # View the service definition
systemctl list-dependencies rpcbind # See what depends on it
Complete SSH Hardening Configuration
SSH is the primary remote entry point for Linux servers and the top target for attackers. Below is the recommended hardening configuration for production environments:
# /etc/ssh/sshd_config
Port 22022 # Change the default port (reduces automated scan noise)
# Protocol and encryption
Protocol 2
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# Authentication
PermitRootLogin no # Disable direct root login
PasswordAuthentication no # Disable password authentication (key-only)
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3 # Maximum authentication attempts
LoginGraceTime 30 # Authentication timeout 30 seconds
# Access control
AllowGroups sshusers # Only allow members of the sshusers group
# Session management
ClientAliveInterval 300 # Send heartbeat after 5 minutes of inactivity
ClientAliveCountMax 2 # Disconnect after 2 missed responses
X11Forwarding no # Disable X11 forwarding
AllowTcpForwarding no # Disable TCP forwarding (enable as needed)
AllowAgentForwarding no # Disable Agent forwarding
# Logging
LogLevel VERBOSE # Log detailed information (including key fingerprints)
Create the SSH user group and configure keys:
# Create a dedicated SSH user group
groupadd sshusers
# Add operations users to the group
usermod -aG sshusers ops_user
# Configure key authentication
mkdir -p /home/ops_user/.ssh
chmod 700 /home/ops_user/.ssh
# Write the public key
echo "ssh-ed25519 AAAAC3Nza... user@workstation" > /home/ops_user/.ssh/authorized_keys
chmod 600 /home/ops_user/.ssh/authorized_keys
chown -R ops_user:ops_user /home/ops_user/.ssh
# Always validate configuration syntax before restarting sshd
sshd -t && systemctl restart sshd
Fail2ban Brute Force Protection
Even with a changed port, targeted scans are still possible. Fail2ban monitors logs and automatically bans malicious IPs:
# Install
dnf install -y fail2ban # RHEL
apt install -y fail2ban # Debian/Ubuntu
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
bantime.increment = true
bantime.factor = 2
banaction = firewallcmd-ipset
[sshd]
enabled = true
port = 22022
filter = sshd
logpath = %(sshd_log)s
backend = systemd
systemctl enable --now fail2ban
# View ban status
fail2ban-client status sshd
Important: After modifying SSH configuration, do not close the current terminal. Open a new terminal to verify that the new configuration works before exiting the old session, to avoid locking yourself out.
SELinux/AppArmor Mandatory Access Control
SELinux (RHEL/CentOS/Fedora)
SELinux provides fine-grained access control over processes, files, and ports through type enforcement. Production environments should maintain Enforcing mode, rather than simply disabling it.
# View current mode
getenforce
# Enforcing / Permissive / Disabled
# View detailed status
sestatus
# Temporarily switch mode (lost on reboot)
setenforce 0 # Permissive (logs only, no blocking)
setenforce 1 # Enforcing (actual blocking)
# Permanent setting
sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
When a service fails to work properly due to SELinux, the correct troubleshooting approach is to analyze the audit logs rather than disable SELinux:
# View recent AVC denial logs
ausearch -m AVC -ts recent
# More user-friendly analysis tool
yum install -y setroubleshoot-server
sealert -a /var/log/audit/audit.log
# If a policy gap is confirmed, generate and apply a custom policy module
ausearch -m AVC -ts today | audit2allow -M mycustompol
semodule -i mycustompol.pp
Common SELinux context adjustments:
# Assign a non-standard port to the HTTP service
semanage port -a -t http_port_t -p tcp 8080
# Modify the SELinux context of a file
semanage fcontext -a -t httpd_sys_content_t "/data/web(/.*)?"
restorecon -Rv /data/web
AppArmor (Ubuntu/Debian)
AppArmor uses path-based access control, with a more intuitive configuration than SELinux:
# View status
apparmor_status
# View the AppArmor profile of a process
aa-status | grep <process_name>
# List all loaded profiles
ls /etc/apparmor.d/
# Switch a profile to complain mode (logs only, no blocking)
aa-complain /etc/apparmor.d/usr.sbin.mysqld
# Switch back to enforce mode
aa-enforce /etc/apparmor.d/usr.sbin.mysqld
auditd Audit System Configuration
auditd is the userspace component of the Linux kernel auditing framework. It records system calls and key file access, and is the core tool for compliance auditing.
Installation and Configuration
# Install
dnf install -y audit # RHEL
apt install -y auditd # Debian/Ubuntu
systemctl enable --now auditd
# /etc/audit/auditd.conf key parameters
log_file = /var/log/audit/audit.log
log_format = ENRISHED
# Log rotation: rotate after reaching 8MB, keep 5 copies
max_log_file = 8
num_logs = 5
max_log_file_action = ROTATE
# Behavior when disk is full
disk_full_action = HALT # Halt the system when disk is full (ensures no audit loss, compliance requirement)
Audit Rule Configuration
# /etc/audit/rules.d/audit.rules
# 1. Monitor login-related file tampering
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k identity
# 2. Monitor SSH configuration changes
-w /etc/ssh/sshd_config -p wa -k ssh_config
# 3. Monitor sudo usage
-w /var/log/sudo.log -p wa -k sudo_usage
# 4. Monitor system time changes
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
# 5. Monitor network environment changes
-w /etc/hosts -p wa -k system_config
-w /etc/sysconfig/network -p wa -k system_config
# 6. Monitor user/group command execution
-a always,exit -F arch=b64 -S execve -F uid>=1000 -k user_commands
# 7. Monitor kernel module loading
-w /sbin/insmod -p x -k modules
-w /sbin/rmmod -p x -k modules
-w /sbin/modprobe -p x -k modules
# Reload rules
augenrules --load
# Verify rules are in effect
auditctl -l
Log Analysis
# View all audit events from the last 10 minutes
ausearch -ts recent
# Query by key
ausearch -k identity
# View all operations by a user
ausearch -ui 1000
# Generate readable reports
aureport --summary
aureport -k # Summary by key
aureport -f # Summary by file
Firewall Rule Management
firewalld (RHEL/CentOS/Fedora)
# View current zones and rules
firewall-cmd --get-default-zone
firewall-cmd --list-all
# Set the default zone to public
firewall-cmd --set-default-zone=public
# Allow business ports (permanent)
firewall-cmd --permanent --add-port=22022/tcp # SSH
firewall-cmd --permanent --add-port=443/tcp # HTTPS
firewall-cmd --permanent --add-port=8080-8090/tcp # Port range
# Restrict SSH source IP (only allow ops network)
firewall-cmd --permanent --new-zone=ssh-restricted
firewall-cmd --permanent --zone=ssh-restricted --add-source=10.0.1.0/24
firewall-cmd --permanent --zone=ssh-restricted --add-port=22022/tcp
firewall-cmd --permanent --zone=public --remove-port=22022/tcp
# Reload
firewall-cmd --reload
# Verify
firewall-cmd --list-all --zone=ssh-restricted
nftables (Debian/Ubuntu/Modern Linux)
nftables is the successor to iptables, with a more unified configuration syntax:
# /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow loopback
iif "lo" accept
# Allow established connection return traffic
ct state established,related accept
# Drop invalid connections
ct state invalid drop
# Allow ICMP (rate-limited to prevent flood)
icmp type echo-request limit rate 5/second accept
ip6 nexthdr icmpv6 accept
# SSH rate limiting: max 5 new connections per IP per minute
tcp dport 22022 ct state new limit rate 5/minute accept
# HTTPS
tcp dport { 80, 443 } accept
# Log and drop by default
limit rate 10/minute log prefix "nft-drop: " drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
# Apply configuration
nft -f /etc/nftables.conf
systemctl enable nftables
# View the current ruleset
nft list ruleset
# View rule match counters
nft list ruleset -a
Security Baseline Check Script
The following script integrates the above checks and can be used as a periodic audit tool. It performs read-only checks and does not modify any system configuration:
#!/bin/bash
# security_baseline_check.sh - Linux Security Baseline Check Script
# Usage: sudo bash security_baseline_check.sh
# Reference: CIS Benchmark
set -euo pipefail
PASS=0
FAIL=0
WARN=0
REPORT="/tmp/security_baseline_$(date +%Y%m%d_%H%M%S).txt"
check() {
local desc="$1"
local result="$2"
local detail="${3:-}"
if [ "$result" = "PASS" ]; then
echo "[PASS] $desc" | tee -a "$REPORT"
PASS=$((PASS + 1))
elif [ "$result" = "FAIL" ]; then
echo "[FAIL] $desc - $detail" | tee -a "$REPORT"
FAIL=$((FAIL + 1))
else
echo "[WARN] $desc - $detail" | tee -a "$REPORT"
WARN=$((WARN + 1))
fi
}
echo "===============================" | tee "$REPORT"
echo "Linux Security Baseline Check Report" | tee -a "$REPORT"
echo "Host: $(hostname)" | tee -a "$REPORT"
echo "Time: $(date)" | tee -a "$REPORT"
echo "===============================" | tee -a "$REPORT"
# --- 1. System Updates ---
if dnf check-update >/dev/null 2>&1 || apt list --upgradable 2>/dev/null | grep -q upgradable; then
check "System patches up to date" "WARN" "Pending package updates"
else
check "System patches up to date" "PASS"
fi
# --- 2. SSH Hardening ---
SSHD_CONFIG="/etc/ssh/sshd_config"
if grep -q "^PermitRootLogin.*no" "$SSHD_CONFIG"; then
check "SSH root login disabled" "PASS"
else
check "SSH root login disabled" "FAIL" "PermitRootLogin no not configured"
fi
if grep -q "^PasswordAuthentication.*no" "$SSHD_CONFIG"; then
check "SSH password authentication disabled" "PASS"
else
check "SSH password authentication disabled" "FAIL" "PasswordAuthentication no not configured"
fi
if grep -q "^Protocol.*2" "$SSHD_CONFIG" 2>/dev/null || true; then
check "SSH protocol version" "PASS"
else
# OpenSSH 7.0+ defaults to Protocol 2 only, this check can be skipped
check "SSH protocol version" "WARN" "Not explicitly configured (7.0+ defaults to Protocol 2)"
fi
if grep -q "^MaxAuthTries" "$SSHD_CONFIG"; then
max_tries=$(grep "^MaxAuthTries" "$SSHD_CONFIG" | awk '{print $2}')
if [ "$max_tries" -le 4 ]; then
check "SSH MaxAuthTries ($max_tries)" "PASS"
else
check "SSH MaxAuthTries ($max_tries)" "WARN" "Value too high, recommend <=4"
fi
else
check "SSH MaxAuthTries" "WARN" "Not configured (default 6)"
fi
# --- 3. Account Security ---
# Check for empty password accounts
EMPTY_PW=$(awk -F: '($2 == "" || $2 == "!") {print $1}' /etc/shadow 2>/dev/null | wc -l)
if [ "$EMPTY_PW" -eq 0 ]; then
check "No empty password accounts" "PASS"
else
check "No empty password accounts" "FAIL" "Found $EMPTY_PW empty password accounts"
fi
# Check for UID=0 accounts (should only be root)
ROOT_UID=$(awk -F: '($3 == 0) {print $1}' /etc/passwd)
if [ "$ROOT_UID" = "root" ]; then
check "UID=0 is root only" "PASS"
else
check "UID=0 is root only" "FAIL" "Multiple UID=0 accounts: $ROOT_UID"
fi
# Password policy
if [ -f /etc/security/pwquality.conf ]; then
MINLEN=$(grep "^minlen" /etc/security/pwquality.conf 2>/dev/null | awk '{print $2}' || echo 0)
if [ "${MINLEN:-0}" -ge 12 ]; then
check "Minimum password length ($MINLEN)" "PASS"
else
check "Minimum password length ($MINLEN)" "WARN" "Recommend >=12"
fi
fi
# --- 4. Firewall ---
if systemctl is-active --quiet firewalld 2>/dev/null; then
check "firewalld is running" "PASS"
elif systemctl is-active --quiet nftables 2>/dev/null; then
check "nftables is running" "PASS"
elif systemctl is-active --quiet ufw 2>/dev/null; then
check "ufw is running" "PASS"
else
check "Firewall running status" "FAIL" "No running firewall detected"
fi
# --- 5. SELinux/AppArmor ---
if command -v getenforce >/dev/null 2>&1; then
SELINUX_MODE=$(getenforce)
if [ "$SELINUX_MODE" = "Enforcing" ]; then
check "SELinux mode (Enforcing)" "PASS"
elif [ "$SELINUX_MODE" = "Permissive" ]; then
check "SELinux mode (Permissive)" "WARN" "Recommend switching to Enforcing"
else
check "SELinux mode (Disabled)" "FAIL" "SELinux is disabled"
fi
elif command -v apparmor_status >/dev/null 2>&1; then
AA_PROFILES=$(apparmor_status 2>/dev/null | grep "profiles are loaded" | awk '{print $1}')
if [ "${AA_PROFILES:-0}" -gt 0 ]; then
check "AppArmor profiles ($AA_PROFILES)" "PASS"
else
check "AppArmor profiles" "FAIL" "No loaded profiles"
fi
fi
# --- 6. auditd ---
if systemctl is-active --quiet auditd 2>/dev/null; then
check "auditd is running" "PASS"
else
check "auditd running status" "WARN" "auditd is not running"
fi
# --- 7. Dangerous Services ---
DANGER_SERVICES="telnet rsh rlogin tftp xinetd ypbind"
for svc in $DANGER_SERVICES; do
if systemctl is-enabled "$svc" 2>/dev/null | grep -q enabled; then
check "Dangerous service $svc not enabled" "FAIL" "$svc is enabled"
fi
done
check "Dangerous service check completed" "PASS"
# --- 8. Kernel Parameter Hardening ---
SYNCOOKIES=$(sysctl -n net.ipv4.tcp_syncookies 2>/dev/null || echo 0)
if [ "$SYNCOOKIES" = "1" ]; then
check "TCP SYN Cookies enabled" "PASS"
else
check "TCP SYN Cookies enabled" "WARN" "Recommend enabling to prevent SYN Flood"
fi
IP_FORWARD=$(sysctl -n net.ipv4.ip_forward 2>/dev/null || echo 0)
if [ "$IP_FORWARD" = "0" ]; then
check "IP forwarding disabled" "PASS"
else
check "IP forwarding disabled" "WARN" "Non-routers should disable ip_forward"
fi
# --- 9. File Permissions ---
# Check /etc/passwd permissions
PASSWD_PERM=$(stat -c %a /etc/passwd)
if [ "$PASSWD_PERM" = "644" ]; then
check "/etc/passwd permissions (644)" "PASS"
else
check "/etc/passwd permissions ($PASSWD_PERM)" "WARN" "Standard is 644"
fi
# Check /etc/shadow permissions
SHADOW_PERM=$(stat -c %a /etc/shadow)
if [ "$SHADOW_PERM" = "000" ] || [ "$SHADOW_PERM" = "640" ]; then
check "/etc/shadow permissions ($SHADOW_PERM)" "PASS"
else
check "/etc/shadow permissions ($SHADOW_PERM)" "WARN" "Recommend 000 or 640"
fi
# --- Summary ---
echo "===============================" | tee -a "$REPORT"
echo "Summary: PASS=$PASS FAIL=$FAIL WARN=$WARN" | tee -a "$REPORT"
echo "Detailed report saved: $REPORT" | tee -a "$REPORT"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
Deployment and usage:
# Grant execute permission
chmod +x security_baseline_check.sh
# Run the check (recommend running as root)
sudo ./security_baseline_check.sh
# Configure scheduled checks (every Monday at 8 AM)
# crontab -e
0 8 * * 1 /opt/scripts/security_baseline_check.sh >> /var/log/security_baseline.log 2>&1
Hardening Checklist Summary
| Category | Key Measures | Priority |
|---|---|---|
| System minimization | Minimal install, trim services, remove dangerous packages | High |
| SSH hardening | Key auth, change port, disable root, Fail2ban | High |
| Mandatory access control | SELinux Enforcing / AppArmor | High |
| Audit system | Configure auditd rules, regular analysis | Medium |
| Firewall | Default deny, minimal port exposure | High |
| Password policy | Minimum length 12, complexity, periodic checks | Medium |
| Kernel hardening | SYN Cookies, disable IP forwarding, core parameters | Medium |
| Periodic auditing | Baseline script on schedule, vulnerability patching | Medium |
Security hardening is never a one-and-done task. We recommend incorporating the baseline check script into CI/CD or scheduled tasks, automatically executing after every system change to ensure the security posture continuously meets expectations.