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_authenticatorfile: 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
firewalldinstead 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.