TL;DR — Quick Summary

A comprehensive 20-step security checklist for Linux servers covering updates, SSH, firewall, user management, auditing, and intrusion detection.

LINUX SERVER SECURITY LAYERS System Updates, Kernel, Audit Access Control SSH, sudo, 2FA, Keys Network Firewall, Ports, VPN Filesystem Permissions, Encryption Monitoring Logs, IDS, Alerts Defense in depth: multiple security layers protect your server

Every Linux server connected to the internet is a target. Automated bots scan the entire IPv4 address space in minutes, probing for default credentials, unpatched services, and misconfigured firewalls. Whether you are running a production web server, a development environment, or a home lab, a systematic approach to security is essential.

This article provides a 20-step security checklist organized into five sections: System, Access, Network, Filesystem, and Monitoring/Maintenance. Each step includes the actual commands you need to run on Ubuntu/Debian systems (with notes for RHEL/CentOS where the commands differ). Bookmark this page and work through it every time you provision a new server.


Section 1: System Hardening

Step 1: Keep the System Updated

The single most important security measure is keeping all packages up to date. Most exploits target known vulnerabilities that already have patches available.

# Update package lists and upgrade all packages
sudo apt update && sudo apt upgrade -y

# On RHEL/CentOS/Fedora
sudo dnf update -y

Check for pending updates regularly:

# List upgradable packages
apt list --upgradable

Best Practice: Schedule updates during maintenance windows, but never delay security patches by more than 48 hours. The window between a CVE disclosure and active exploitation is shrinking every year.

Step 2: Enable Automatic Security Updates

For unattended security patches, configure unattended-upgrades:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Verify the configuration:

cat /etc/apt/apt.conf.d/20auto-upgrades

Expected output:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

Fine-tune which updates are applied automatically in /etc/apt/apt.conf.d/50unattended-upgrades:

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}";
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};

// Email notifications
Unattended-Upgrade::Mail "admin@knowledgexchange.xyz";

// Auto-reboot if required (with timing)
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

Step 3: Remove Unnecessary Packages

Every installed package is a potential attack surface. Remove what you do not need:

# List manually installed packages
apt-mark showmanual

# Remove a package and its configuration files
sudo apt purge -y <package-name>

# Clean up orphaned dependencies
sudo apt autoremove -y

Common packages to evaluate for removal on servers:

# Desktop environments (should never be on a server)
sudo apt purge -y ubuntu-desktop gdm3 gnome-shell

# Telnet (use SSH instead)
sudo apt purge -y telnet telnetd

# rsh/rlogin (insecure remote access)
sudo apt purge -y rsh-client rsh-server

Step 4: Disable Unused Services

List all active services and disable those you do not need:

# List all enabled services
systemctl list-unit-files --type=service --state=enabled

# Disable and stop an unnecessary service
sudo systemctl disable --now cups.service        # Print service
sudo systemctl disable --now avahi-daemon.service # mDNS/Bonjour
sudo systemctl disable --now bluetooth.service    # Bluetooth

Check for services listening on network ports:

sudo ss -tlnp

Any service listening on 0.0.0.0 or :: is accessible from the network. If you do not need it, disable it or bind it to 127.0.0.1.


Section 2: Access Control

Step 5: SSH Hardening

SSH is the primary remote access method and the most commonly attacked service. Harden it aggressively.

Edit /etc/ssh/sshd_config:

sudo nano /etc/ssh/sshd_config

Apply these settings:

# Disable root login
PermitRootLogin no

# Disable password authentication (use SSH keys only)
PasswordAuthentication no

# Disable empty passwords
PermitEmptyPasswords no

# Use SSH Protocol 2 only
Protocol 2

# Change the default port (optional but reduces noise)
Port 2222

# Limit authentication attempts
MaxAuthTries 3

# Set login grace period
LoginGraceTime 30

# Disable X11 forwarding unless needed
X11Forwarding no

# Disable TCP forwarding unless needed
AllowTcpForwarding no

# Limit SSH to specific users
AllowUsers deploy admin

# Use strong key exchange algorithms
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256@libssh.org,curve25519-sha256

# Use strong ciphers
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com

# Use strong MACs
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

# Idle timeout (disconnect after 5 minutes of inactivity)
ClientAliveInterval 300
ClientAliveCountMax 0

Restart SSH:

sudo systemctl restart sshd

Warning: Before disabling password authentication, ensure you have a working SSH key pair configured. Test key-based login in a new terminal session before closing your current connection. Getting locked out of a remote server is not recoverable without console access.

Step 6: Use sudo Instead of Root

Never log in as root. Create an administrative user with sudo privileges:

# Create a new admin user
sudo adduser admin

# Add to the sudo group
sudo usermod -aG sudo admin

# Verify sudo access
su - admin
sudo whoami  # Should output: root

Lock the root account from direct login:

sudo passwd -l root

Step 7: Strong Password Policies

Even with SSH key authentication, enforce strong passwords for local accounts and sudo:

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

Configure /etc/security/pwquality.conf:

# Minimum password length
minlen = 14

# Require at least one digit
dcredit = -1

# Require at least one uppercase letter
ucredit = -1

# Require at least one lowercase letter
lcredit = -1

# Require at least one special character
ocredit = -1

# Maximum consecutive identical characters
maxrepeat = 3

# Reject passwords containing the username
usercheck = 1

Set password aging policies:

# Set password expiration for existing users
sudo chage -M 90 -m 7 -W 14 admin

# View password policy for a user
sudo chage -l admin

Step 8: Two-Factor Authentication

Add a second factor to SSH login using Google Authenticator (TOTP):

sudo apt install -y libpam-google-authenticator

Run the setup as the user you want to protect:

google-authenticator

Answer the prompts:

  • Time-based tokens: Yes
  • Update .google_authenticator file: Yes
  • Disallow multiple uses of same token: Yes
  • Allow 30-second window: Yes
  • Enable rate limiting: Yes

Configure PAM for SSH. Edit /etc/pam.d/sshd:

# Add at the end
auth required pam_google_authenticator.so

Enable challenge-response in /etc/ssh/sshd_config:

ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

Restart SSH:

sudo systemctl restart sshd

Now SSH login requires both a valid SSH key and a TOTP code.


Section 3: Network Security

Step 9: Configure Firewall (UFW)

UFW (Uncomplicated Firewall) is the standard firewall for Ubuntu. Enable it with a deny-all-incoming default:

# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (adjust port if you changed it)
sudo ufw allow 2222/tcp comment 'SSH'

# Allow HTTP and HTTPS if running a web server
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

# Enable the firewall
sudo ufw enable

# Check status
sudo ufw status verbose

Rate-limit SSH connections to slow down brute-force attacks:

# Allow max 6 connections per 30 seconds from a single IP
sudo ufw limit 2222/tcp comment 'SSH rate limit'

Tip: On RHEL/CentOS systems, use firewalld instead of UFW. The concepts are identical: default-deny incoming, explicitly allow only needed services.

Step 10: Disable IPv6 If Unused

If you are not actively using IPv6, disable it to reduce the attack surface:

# Add to /etc/sysctl.d/99-disable-ipv6.conf
sudo tee /etc/sysctl.d/99-disable-ipv6.conf <<EOF
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
EOF

# Apply immediately
sudo sysctl --system

Verify:

cat /proc/sys/net/ipv6/conf/all/disable_ipv6
# Should output: 1

Note: Only disable IPv6 if you are certain none of your services require it. Some applications (like certain Docker configurations) may break without IPv6 loopback.

Step 11: Configure Fail2Ban

Fail2Ban monitors log files and bans IPs that show malicious behavior:

sudo apt install -y fail2ban

Create a local configuration (never edit the main file directly):

sudo tee /etc/fail2ban/jail.local <<EOF
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
banaction = ufw

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

[sshd-ddos]
enabled = true
port = 2222
filter = sshd-ddos
logpath = /var/log/auth.log
maxretry = 5
bantime = 172800
EOF

Start and enable Fail2Ban:

sudo systemctl enable --now fail2ban

# Check jail status
sudo fail2ban-client status
sudo fail2ban-client status sshd

View banned IPs:

sudo fail2ban-client status sshd
# Status for the jail: sshd
# |- Filter
# |  |- Currently failed: 2
# |  |- Total failed:     145
# |  `- File list:        /var/log/auth.log
# `- Actions
#    |- Currently banned:  5
#    |- Total banned:      23
#    `- Banned IP list:    ...

Step 12: Use VPN for Admin Access

For the highest level of security, restrict SSH access to a VPN network only. WireGuard is an excellent choice:

# Install WireGuard
sudo apt install -y wireguard

# Generate server keys
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
chmod 600 /etc/wireguard/server_private.key

Configure /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = <server_private_key>
Address = 10.0.0.1/24
ListenPort = 51820

[Peer]
PublicKey = <client_public_key>
AllowedIPs = 10.0.0.2/32

Then restrict SSH to the VPN interface:

# Remove public SSH access
sudo ufw delete allow 2222/tcp

# Allow SSH only from VPN subnet
sudo ufw allow from 10.0.0.0/24 to any port 2222 proto tcp comment 'SSH via VPN only'

# Allow WireGuard
sudo ufw allow 51820/udp comment 'WireGuard VPN'

Section 4: Filesystem Security

Step 13: Set Proper File Permissions

Ensure critical files and directories have correct ownership and permissions:

# Secure SSH configuration
sudo chmod 600 /etc/ssh/sshd_config
sudo chown root:root /etc/ssh/sshd_config

# Secure authorized_keys files
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

# Secure cron directories
sudo chmod 700 /etc/cron.d
sudo chmod 700 /etc/cron.daily
sudo chmod 700 /etc/cron.hourly
sudo chmod 700 /etc/cron.weekly
sudo chmod 700 /etc/cron.monthly

# Restrict access to su command
sudo chmod 750 /bin/su
sudo dpkg-statoverride --update --add root adm 4750 /bin/su

# Ensure password files have correct permissions
sudo chmod 644 /etc/passwd
sudo chmod 640 /etc/shadow
sudo chown root:shadow /etc/shadow

Find world-writable files (potential security risk):

sudo find / -xdev -type f -perm -0002 -ls 2>/dev/null

Find files with SUID/SGID bits set (potential privilege escalation):

sudo find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -ls 2>/dev/null

Review the output and remove SUID/SGID from any binaries that do not need it:

# Example: remove SUID from a binary
sudo chmod u-s /path/to/unnecessary-suid-binary

Step 14: Enable Disk Encryption

For servers handling sensitive data, use LUKS encryption:

# Check if LUKS is available
sudo apt install -y cryptsetup

# Encrypt a data partition (WARNING: destroys existing data)
sudo cryptsetup luksFormat /dev/sdb1

# Open the encrypted partition
sudo cryptsetup open /dev/sdb1 encrypted_data

# Create a filesystem
sudo mkfs.ext4 /dev/mapper/encrypted_data

# Mount it
sudo mkdir -p /mnt/secure
sudo mount /dev/mapper/encrypted_data /mnt/secure

For full-disk encryption on new installations, select the encryption option during the Ubuntu installer. For existing servers, encrypt data partitions while keeping the boot partition unencrypted.

Note: Full-disk encryption on a remote server means you need a way to enter the decryption passphrase at boot time. Solutions include dropbear-initramfs (SSH into the initramfs to unlock) or remote KVM/IPMI access.

Step 15: Mount /tmp with noexec

Prevent execution of scripts from /tmp, which is a common attack vector:

Edit /etc/fstab:

sudo nano /etc/fstab

Add or modify the /tmp entry:

tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev,size=2G 0 0

If /tmp is not a separate mount point, create a tmpfs mount:

# Apply without rebooting
sudo mount -o remount,noexec,nosuid,nodev /tmp

Also secure /dev/shm:

tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0
sudo mount -o remount,noexec,nosuid,nodev /dev/shm

Verify the mount options:

mount | grep -E '/tmp|/dev/shm'
# tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec,size=2097152k)
# tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,noexec)

Section 5: Monitoring and Maintenance

Step 16: Configure Logging (rsyslog/journald)

Ensure comprehensive logging is enabled and configured to retain logs:

# Check rsyslog is running
sudo systemctl status rsyslog

# Configure journald for persistent storage
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal

Edit /etc/systemd/journald.conf:

[Journal]
Storage=persistent
Compress=yes
SystemMaxUse=500M
SystemMaxFileSize=50M
MaxRetentionSec=90day

Restart journald:

sudo systemctl restart systemd-journald

Configure centralized logging by forwarding to a remote syslog server. Edit /etc/rsyslog.d/50-remote.conf:

# Forward all logs to a central syslog server
*.* @@syslog.knowledgexchange.xyz:514

Step 17: Set Up Log Monitoring (Logwatch)

Logwatch provides daily email summaries of system log activity:

sudo apt install -y logwatch

Configure /etc/logwatch/conf/logwatch.conf:

Output = mail
MailTo = admin@knowledgexchange.xyz
MailFrom = logwatch@yourserver.com
Detail = Med
Range = yesterday
Service = All

Test the report:

sudo logwatch --detail Med --mailto admin@knowledgexchange.xyz --range today

Schedule daily reports via cron (usually auto-configured at installation):

# Verify the cron job exists
ls -la /etc/cron.daily/00logwatch

Step 18: Install Intrusion Detection

AIDE (Advanced Intrusion Detection Environment)

AIDE monitors filesystem changes — it detects when files are modified, added, or deleted:

sudo apt install -y aide

# Initialize the database (takes a few minutes)
sudo aideinit

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

Run a check:

sudo aide --check

Schedule daily checks:

sudo tee /etc/cron.daily/aide-check <<'EOF'
#!/bin/bash
/usr/bin/aide --check | mail -s "AIDE Report for $(hostname)" admin@knowledgexchange.xyz
EOF
sudo chmod +x /etc/cron.daily/aide-check

Important: After legitimate system changes (updates, new software), update the AIDE database: sudo aide --update && sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

rkhunter (Rootkit Hunter)

rkhunter scans for rootkits, backdoors, and local exploits:

sudo apt install -y rkhunter

# Update the database
sudo rkhunter --update

# Set the baseline properties
sudo rkhunter --propupd

# Run a full scan
sudo rkhunter --check --skip-keypress

Configure automatic daily scans in /etc/default/rkhunter:

CRON_DAILY_RUN="yes"
REPORT_EMAIL="admin@knowledgexchange.xyz"
APT_AUTOGEN="yes"

Section 6: Maintenance

Step 19: Regular Backups

A compromised server without backups is a catastrophic event. Implement the 3-2-1 backup strategy: 3 copies, 2 different media, 1 offsite.

Automated Backups with rsync

# Create a backup script
sudo tee /usr/local/bin/server-backup.sh <<'SCRIPT'
#!/bin/bash
set -euo pipefail

BACKUP_DIR="/backup/$(date +%Y-%m-%d)"
REMOTE="backup@offsite-server:/backups/$(hostname)/"
LOG="/var/log/backup.log"

mkdir -p "$BACKUP_DIR"

echo "$(date): Starting backup" >> "$LOG"

# Backup critical directories
rsync -az --delete \
  --exclude='/proc/*' \
  --exclude='/sys/*' \
  --exclude='/tmp/*' \
  --exclude='/dev/*' \
  --exclude='/run/*' \
  /etc/ "$BACKUP_DIR/etc/"

rsync -az --delete /home/ "$BACKUP_DIR/home/"
rsync -az --delete /var/www/ "$BACKUP_DIR/www/"

# Database backup (if applicable)
if command -v mysqldump &>/dev/null; then
  mysqldump --all-databases --single-transaction > "$BACKUP_DIR/all-databases.sql"
fi

# Sync to offsite
rsync -az --delete "$BACKUP_DIR/" "$REMOTE"

echo "$(date): Backup completed successfully" >> "$LOG"
SCRIPT

sudo chmod +x /usr/local/bin/server-backup.sh

Schedule with cron:

# Run daily at 2:00 AM
echo "0 2 * * * root /usr/local/bin/server-backup.sh" | sudo tee /etc/cron.d/server-backup

Verify Backups

Backups that have never been tested are not backups. Schedule monthly recovery tests:

# Test restore of a backup
rsync -az backup@offsite-server:/backups/$(hostname)/latest/etc/ /tmp/backup-test/etc/
diff -r /etc/ /tmp/backup-test/etc/ | head -20

Step 20: Security Audit Schedule

Security is not a one-time task. Establish a regular audit schedule:

Weekly

# Review failed login attempts
sudo journalctl -u sshd --since "7 days ago" | grep "Failed"

# Check for unauthorized user accounts
awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd

# Review sudo usage
sudo journalctl _COMM=sudo --since "7 days ago"

# Check listening ports
sudo ss -tlnp

Monthly

# Run a full system update
sudo apt update && sudo apt upgrade -y

# Run AIDE integrity check
sudo aide --check

# Run rkhunter scan
sudo rkhunter --check --skip-keypress

# Review firewall rules
sudo ufw status verbose

# Check for accounts with empty passwords
sudo awk -F: '($2 == "" ) {print $1}' /etc/shadow

# Review cron jobs for all users
for user in $(cut -f1 -d: /etc/passwd); do
  crontab -l -u "$user" 2>/dev/null | grep -v '^#' | grep -v '^$' && echo "  -- $user"
done

Quarterly

# Full security audit with Lynis
sudo apt install -y lynis
sudo lynis audit system

# Review and rotate SSH keys
# Check key age and consider regenerating keys older than 1 year

# Review and update firewall rules
# Remove rules for services no longer running

# Test backup recovery procedure
# Document any changes to the recovery process

Quick Reference Checklist

Use this condensed checklist when provisioning a new server:

LINUX SERVER SECURITY CHECKLIST
================================

SYSTEM
[ ] 1.  apt update && apt upgrade -y
[ ] 2.  Enable unattended-upgrades
[ ] 3.  Remove unnecessary packages (apt autoremove)
[ ] 4.  Disable unused services (systemctl disable)

ACCESS
[ ] 5.  Harden SSH (key-only, no root, strong ciphers)
[ ] 6.  Create admin user with sudo, lock root
[ ] 7.  Configure password policies (pwquality)
[ ] 8.  Enable 2FA for SSH (google-authenticator)

NETWORK
[ ] 9.  Configure UFW (default deny, allow needed ports)
[ ] 10. Disable IPv6 if unused
[ ] 11. Install and configure Fail2Ban
[ ] 12. Set up VPN for admin access (WireGuard)

FILESYSTEM
[ ] 13. Audit file permissions (SUID, world-writable)
[ ] 14. Enable disk encryption (LUKS) for sensitive data
[ ] 15. Mount /tmp and /dev/shm with noexec

MONITORING
[ ] 16. Configure persistent logging (journald)
[ ] 17. Install Logwatch for daily reports
[ ] 18. Set up AIDE and rkhunter

MAINTENANCE
[ ] 19. Implement automated backups with offsite copy
[ ] 20. Establish weekly/monthly/quarterly audit schedule

Summary

Server security is a continuous process, not a destination. This 20-step checklist covers the fundamental security measures that every Linux server should have in place. The key principles are:

  • Minimize the attack surface — Remove what you do not need (packages, services, open ports)
  • Enforce strong access controls — SSH keys, 2FA, sudo, strong passwords
  • Monitor everything — Logging, intrusion detection, regular audits
  • Prepare for the worst — Tested backups, incident response plans

No single step on this list will make your server impenetrable, but together they create a defense-in-depth strategy that makes exploitation significantly harder and detection significantly faster.

For deeper dives into specific topics covered here, see our articles on creating SSH connections, creating self-signed certificates on Ubuntu, and configuring swappiness on Ubuntu.