Linux Server Hardening: Complete Security Checklist for Production
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
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
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:
- Weekly: Review fail2ban logs, check for failed authentication attempts, verify auditd is running
- Monthly: Run AIDE integrity checks, review SUID binaries, audit user accounts, check for unnecessary services
- Quarterly: Full vulnerability scan with tools like OpenVAS or Nessus, review and update firewall rules, rotate SSH keys
- 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.