Every Linux server exposed to the internet is under constant attack. Automated bots scan IP ranges relentlessly, probing for default credentials, open ports, and unpatched vulnerabilities. A freshly provisioned server with a public IP will typically receive its first SSH brute-force attempt within minutes of going live. Server hardening is not optional; it is the baseline requirement for any production system.

This guide provides a systematic, layered approach to Linux server security. Each section addresses a specific attack surface, and together they form a defense-in-depth strategy that significantly raises the bar for attackers. We assume a Debian/Ubuntu or RHEL/Arch-based system, but the principles apply universally.

Initial System Setup

Before configuring any services, ensure the system itself is in a clean, updated state. Start by applying all available security patches:

# Debian/Ubuntu
apt update && apt full-upgrade -y

# RHEL/CentOS/Fedora
dnf update -y

# Arch Linux
pacman -Syu

Remove any packages you do not need. Every installed package is potential attack surface:

# List installed packages and look for unnecessary ones
dpkg -l | grep -E "^ii" | awk '{print $2}'

# Remove example: telnet, rsh, and other legacy tools
apt purge -y telnet rsh-client rsh-redone-client
apt autoremove -y

User Management and Privilege Control

Never run services as root. Create dedicated service accounts with minimal privileges and restrict who can escalate to root.

Disable Root Login

# Lock the root account password (users can still sudo)
passwd -l root

# Or set the shell to nologin
usermod -s /usr/sbin/nologin root

Create an Admin User with sudo Access

# Create a deployment user
useradd -m -s /bin/bash -G sudo deployer

# Set a strong password
passwd deployer

# Or better yet, set up SSH keys and disable password entirely
mkdir -p /home/deployer/.ssh
chmod 700 /home/deployer/.ssh
echo "ssh-ed25519 AAAA... deployer@workstation" > /home/deployer/.ssh/authorized_keys
chmod 600 /home/deployer/.ssh/authorized_keys
chown -R deployer:deployer /home/deployer/.ssh

Restrict sudo Access

Edit /etc/sudoers using visudo to limit who can use sudo and what they can do:

# Only allow the 'deployer' user to use sudo
# Remove other users from sudo/wheel group
gpasswd -d username sudo

# Require password for sudo (never use NOPASSWD in production)
# In /etc/sudoers:
Defaults    env_reset
Defaults    mail_badpass
Defaults    secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Defaults    logfile="/var/log/sudo.log"
Defaults    log_input, log_output
Defaults    passwd_timeout=1

deployer ALL=(ALL:ALL) ALL

Password Policies

# Install password quality checking
apt install -y libpam-pwquality

# Edit /etc/security/pwquality.conf
minlen = 14
dcredit = -1
ucredit = -1
ocredit = -1
lcredit = -1
maxrepeat = 3
reject_username
enforce_for_root

# Set password aging in /etc/login.defs
PASS_MAX_DAYS   90
PASS_MIN_DAYS   7
PASS_WARN_AGE   14

SSH Hardening

SSH is the most critical service on any Linux server. A misconfigured SSH daemon is the single most common entry point for attackers. Harden it thoroughly.

Edit /etc/ssh/sshd_config:

# Use only SSH Protocol 2
Protocol 2

# Change the default port (obscurity, not security, but reduces noise)
Port 2222

# Restrict to specific listen addresses if applicable
ListenAddress 0.0.0.0

# Authentication
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes

# Limit authentication attempts
MaxAuthTries 3
MaxSessions 3
LoginGraceTime 30

# Restrict users
AllowUsers deployer
# Or restrict by group
# AllowGroups ssh-users

# Disable unnecessary features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
GatewayPorts no
PrintMotd no

# Use strong ciphers and MACs only
Ciphers [email protected],[email protected],[email protected]
MACs [email protected],[email protected]
KexAlgorithms curve25519-sha256,[email protected]

# Idle timeout
ClientAliveInterval 300
ClientAliveCountMax 2

# Logging
LogLevel VERBOSE

After editing, validate the configuration and restart:

sshd -t && systemctl restart sshd
Warning: Before restarting sshd, always ensure you have an alternative way to access the server (console access, out-of-band management, or a second SSH session that remains connected). A misconfigured sshd_config can lock you out permanently.

Firewall Configuration

A firewall is your first line of defense. The principle is simple: deny everything by default, then explicitly allow only what is needed.

Using nftables (Modern Approach)

#!/usr/sbin/nft -f
# /etc/nftables.conf

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Accept established/related connections
        ct state established,related accept

        # Accept loopback
        iif lo accept

        # Drop invalid packets
        ct state invalid drop

        # ICMP (ping) - rate limited
        ip protocol icmp icmp type echo-request limit rate 5/second accept
        ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate 5/second accept

        # SSH on custom port
        tcp dport 2222 ct state new limit rate 3/minute accept

        # HTTP/HTTPS
        tcp dport { 80, 443 } accept

        # Log dropped packets (rate limited to prevent log flooding)
        limit rate 5/minute log prefix "nftables-drop: " level warn
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}
# Enable and start nftables
systemctl enable nftables
systemctl start nftables

# Verify rules
nft list ruleset

Using iptables (Legacy but Widely Supported)

# Flush existing rules
iptables -F
iptables -X

# Default policies: drop everything
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Allow loopback
iptables -A INPUT -i lo -j ACCEPT

# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Drop invalid
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

# SSH rate limiting
iptables -A INPUT -p tcp --dport 2222 -m conntrack --ctstate NEW \
  -m recent --set --name SSH
iptables -A INPUT -p tcp --dport 2222 -m conntrack --ctstate NEW \
  -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP
iptables -A INPUT -p tcp --dport 2222 -j ACCEPT

# HTTP/HTTPS
iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT

# ICMP rate limit
iptables -A INPUT -p icmp --icmp-type echo-request \
  -m limit --limit 5/s -j ACCEPT

# Log and drop everything else
iptables -A INPUT -m limit --limit 5/min -j LOG \
  --log-prefix "iptables-drop: " --log-level 4
iptables -A INPUT -j DROP

# Save rules
iptables-save > /etc/iptables/rules.v4

Fail2ban Configuration

Fail2ban monitors log files and temporarily bans IP addresses that show malicious behavior, such as repeated failed login attempts:

# Install
apt install -y fail2ban

# Create local configuration (never edit jail.conf directly)
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Edit /etc/fail2ban/jail.local:

[DEFAULT]
bantime  = 3600
findtime = 600
maxretry = 3
banaction = nftables-multiport
backend = systemd
ignoreip = 127.0.0.1/8 ::1

# Email notifications
destemail = [email protected]
sender = [email protected]
action = %(action_mwl)s

[sshd]
enabled = true
port    = 2222
filter  = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400

[sshd-aggressive]
enabled  = true
port     = 2222
filter   = sshd[mode=aggressive]
logpath  = /var/log/auth.log
maxretry = 1
bantime  = 604800
# Start and enable
systemctl enable fail2ban
systemctl start fail2ban

# Check status
fail2ban-client status sshd

Kernel Hardening with sysctl

The Linux kernel exposes many tunable parameters that can significantly improve security. Create /etc/sysctl.d/99-hardening.conf:

# IP Spoofing protection
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Disable IP source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0

# Disable ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0

# Enable TCP SYN cookies (SYN flood protection)
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2

# Log martian packets
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1

# Disable IPv6 if not needed
# net.ipv6.conf.all.disable_ipv6 = 1
# net.ipv6.conf.default.disable_ipv6 = 1

# Protect against time-wait assassination
net.ipv4.tcp_rfc1337 = 1

# Ignore ICMP broadcast requests
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Ignore bogus ICMP error responses
net.ipv4.icmp_ignore_bogus_error_responses = 1

# ASLR (Address Space Layout Randomization)
kernel.randomize_va_space = 2

# Restrict kernel pointer access
kernel.kptr_restrict = 2

# Restrict dmesg access to root
kernel.dmesg_restrict = 1

# Restrict ptrace scope
kernel.yama.ptrace_scope = 2

# Disable core dumps
fs.suid_dumpable = 0

# Restrict unprivileged BPF
kernel.unprivileged_bpf_disabled = 1

# Restrict userfaultfd to privileged users
vm.unprivileged_userfaultfd = 0
# Apply immediately
sysctl --system
Tip: If your server runs Docker containers, be cautious with net.ipv4.ip_forward -- Docker requires this to be set to 1. Do not disable IP forwarding on a Docker host. See our Linux Kernel Tuning guide for Docker-specific sysctl parameters.

Audit Logging with auditd

The Linux Audit Framework provides detailed logging of system calls, file access, and security events. It is essential for compliance (PCI-DSS, HIPAA, SOC 2) and incident forensics.

# Install auditd
apt install -y auditd audispd-plugins

# Enable and start
systemctl enable auditd
systemctl start auditd

Create audit rules in /etc/audit/rules.d/hardening.rules:

# Delete all existing rules
-D

# Set buffer size
-b 8192

# Failure mode: 1 = printk, 2 = panic
-f 1

# Monitor changes to authentication files
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers

# Monitor SSH configuration changes
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config

# Monitor cron configuration
-w /etc/crontab -p wa -k cron
-w /etc/cron.d/ -p wa -k cron
-w /var/spool/cron/ -p wa -k cron

# Monitor login/logout events
-w /var/log/lastlog -p wa -k logins
-w /var/run/faillock/ -p wa -k logins

# Monitor privilege escalation
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=-1 -k privilege_escalation

# Monitor file deletion by users
-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat \
  -F auid>=1000 -F auid!=-1 -k file_deletion

# Monitor kernel module operations
-w /sbin/insmod -p x -k modules
-w /sbin/modprobe -p x -k modules
-w /sbin/rmmod -p x -k modules
-a always,exit -F arch=b64 -S init_module -S delete_module -k modules

# Monitor network configuration changes
-w /etc/hosts -p wa -k network_config
-w /etc/network/ -p wa -k network_config
-w /etc/sysctl.conf -p wa -k sysctl
-w /etc/sysctl.d/ -p wa -k sysctl

# Make audit configuration immutable (requires reboot to change)
-e 2
# Load the rules
augenrules --load

# Search audit logs
ausearch -k identity --start today
aureport --auth --summary

File Permissions and Integrity

Proper file permissions prevent unauthorized access and privilege escalation:

# Restrict permissions on sensitive files
chmod 600 /etc/shadow
chmod 600 /etc/gshadow
chmod 644 /etc/passwd
chmod 644 /etc/group
chmod 600 /etc/ssh/sshd_config
chmod 700 /root

# Find world-writable files (potential security risk)
find / -xdev -type f -perm -0002 -ls 2>/dev/null

# Find SUID/SGID binaries (potential privilege escalation)
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -ls 2>/dev/null

# Remove unnecessary SUID bits
chmod u-s /usr/bin/newgrp
chmod u-s /usr/bin/chsh
chmod u-s /usr/bin/chfn

File Integrity Monitoring with AIDE

# Install AIDE
apt install -y aide

# Initialize the database
aideinit

# Copy the new database
cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# Run a check
aide --check

# Schedule daily checks via cron
echo '0 5 * * * root /usr/bin/aide --check | mail -s "AIDE Report" [email protected]' \
  >> /etc/crontab

Mandatory Access Control: AppArmor and SELinux

Discretionary access control (standard Unix permissions) is not enough. Mandatory Access Controls (MAC) confine processes to a minimum set of privileges, even if they are compromised.

AppArmor (Debian/Ubuntu Default)

# Check AppArmor status
aa-status

# Install additional profiles
apt install -y apparmor-profiles apparmor-profiles-extra apparmor-utils

# Put a profile in enforce mode
aa-enforce /etc/apparmor.d/usr.sbin.sshd

# Generate a profile for a new application
aa-genprof /usr/local/bin/myapp

# Scan for unconfined processes
aa-unconfined --paranoid

SELinux (RHEL/CentOS/Fedora)

# Check SELinux status
getenforce
sestatus

# Set to enforcing mode
setenforce 1

# Make permanent in /etc/selinux/config
# SELINUX=enforcing

# View denials
ausearch -m AVC -ts recent

# Generate policy module for a denial
ausearch -m AVC -ts recent | audit2allow -M mypolicy
semodule -i mypolicy.pp

# List booleans
getsebool -a | grep httpd

# Toggle a boolean
setsebool -P httpd_can_network_connect on

Automatic Security Updates

Unpatched vulnerabilities are one of the most common attack vectors. Enable automatic security updates:

Debian/Ubuntu

# Install unattended-upgrades
apt install -y unattended-upgrades apt-listchanges

# Configure
dpkg-reconfigure -plow unattended-upgrades

# Edit /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Mail "[email protected]";

Arch Linux

# Arch does not have traditional unattended upgrades
# Use a systemd timer for controlled updates
# See our Arch Linux Server Setup guide for details

# Install pacman-contrib for paccache
pacman -S pacman-contrib

# Create a systemd timer for checking updates
# /etc/systemd/system/update-check.timer
[Unit]
Description=Check for package updates

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Additional Hardening Measures

Disable Unnecessary Services

# List all running services
systemctl list-units --type=service --state=running

# Disable unused services
systemctl disable --now avahi-daemon
systemctl disable --now cups
systemctl disable --now bluetooth
systemctl disable --now rpcbind

# Mask services to prevent them from starting
systemctl mask rpcbind

Configure Login Banners

# /etc/issue.net (displayed before SSH login)
Authorized access only. All activity is monitored and logged.

# /etc/motd (displayed after login)
This system is monitored. Unauthorized access will be prosecuted.

Restrict Compiler Access

# Remove compilers if not needed on production servers
apt purge -y gcc g++ make
# Or restrict access
chmod 700 /usr/bin/gcc
chmod 700 /usr/bin/g++

USB and Physical Device Hardening

# Disable USB storage (prevents data exfiltration via USB)
echo "blacklist usb_storage" > /etc/modprobe.d/disable-usb-storage.conf
echo "install usb_storage /bin/true" >> /etc/modprobe.d/disable-usb-storage.conf

Security Hardening Verification Checklist

Use this table to verify your hardening measures are in place:

Category Check Command
SSH Root login disabled grep PermitRootLogin /etc/ssh/sshd_config
SSH Password auth disabled grep PasswordAuthentication /etc/ssh/sshd_config
Firewall Default deny policy nft list chain inet filter input
Fail2ban SSH jail active fail2ban-client status sshd
Kernel ASLR enabled sysctl kernel.randomize_va_space
Kernel SYN cookies enabled sysctl net.ipv4.tcp_syncookies
Audit auditd running systemctl is-active auditd
MAC AppArmor/SELinux active aa-status or getenforce
Updates Auto-updates configured systemctl is-active unattended-upgrades
Files No world-writable files find / -xdev -type f -perm -0002

Defense in depth: No single measure is sufficient. Security comes from layering multiple controls so that a failure in one layer is caught by the next. A hardened SSH configuration combined with a firewall, fail2ban, audit logging, and mandatory access controls creates overlapping barriers that an attacker must defeat simultaneously.

Ongoing Maintenance

Hardening is not a one-time event. Schedule regular security reviews:

  1. Weekly: Review fail2ban logs, check for failed authentication attempts, verify auditd is running
  2. Monthly: Run AIDE integrity checks, review SUID binaries, audit user accounts, check for unnecessary services
  3. Quarterly: Full vulnerability scan with tools like OpenVAS or Nessus, review and update firewall rules, rotate SSH keys
  4. After every incident: Full forensic review using audit logs, update rules and configurations based on findings

If you manage Docker containers on your hardened servers, platforms like usulnet provide security scanning and container hardening features that complement the host-level controls described here, giving you visibility into both host and container security posture from a single interface.

Tip: Automate your hardening verification by scripting the checklist above and running it via a systemd timer. Store results in a central logging system so you can detect configuration drift immediately.