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

CategoryKey MeasuresPriority
System minimizationMinimal install, trim services, remove dangerous packagesHigh
SSH hardeningKey auth, change port, disable root, Fail2banHigh
Mandatory access controlSELinux Enforcing / AppArmorHigh
Audit systemConfigure auditd rules, regular analysisMedium
FirewallDefault deny, minimal port exposureHigh
Password policyMinimum length 12, complexity, periodic checksMedium
Kernel hardeningSYN Cookies, disable IP forwarding, core parametersMedium
Periodic auditingBaseline script on schedule, vulnerability patchingMedium

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.

References