Security

Linux Server Hardening Checklist: Secure Your VPS in 2026

2026-07-26·14 min read
#linux#security#server-hardening#sysadmin#vps

Linux Server Hardening Checklist: Secure Your VPS in 2026

Every unhardened Linux server on the public internet gets scanned within minutes of going online. Bots probe for open ports, default credentials, and known vulnerabilities 24/7. If you're spinning up a VPS for your side project, startup, or production workload, this checklist will save you from becoming another statistic.

This guide is practical — every step includes the actual commands you need to run. No theory, just action.


Why Server Hardening Matters

Default Linux installations are configured for convenience, not security. Out of the box, most servers:

  • Allow root SSH login with password authentication
  • Run unnecessary services (Avahi, CUPS, etc.)
  • Don't have a firewall configured
  • Don't log security events
  • Use outdated kernel parameters
  • Allow all outbound traffic by default

Hardening takes 30-60 minutes and prevents 90%+ of automated attacks. Do it on every new server, before you deploy anything.


Phase 1: Initial Setup (First 5 Minutes)

1.1 Update Everything

# Debian/Ubuntu
sudo apt update && sudo apt upgrade -y
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

# RHEL/CentOS/AlmaLinux/Rocky
sudo dnf update -y
sudo dnf install -y dnf-automatic
sudo systemctl enable --now dnf-automatic

1.2 Create a Non-Root User

# Create deploy user
sudo adduser deploy
sudo usermod -aG sudo deploy

# Set a strong password
sudo passwd deploy

# Copy SSH keys to the new user
sudo mkdir -p /home/deploy/.ssh
sudo cp ~/.ssh/authorized_keys /home/deploy/.ssh/
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys

1.3 Generate and Install SSH Keys (if not done)

# On your LOCAL machine (not the server)
ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/server_key

# Copy the public key to the server
ssh-copy-id -i ~/.ssh/server_key.pub deploy@your_server_ip

Verify you can log in as deploy with your SSH key before continuing.


Phase 2: SSH Hardening

2.1 Disable Root Login and Password Authentication

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo tee /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
# Disable root login
PermitRootLogin no

# Disable password authentication
PasswordAuthentication no
KbdInteractiveAuthentication no

# Only allow key-based auth
PubkeyAuthentication yes

# Limit to specific users
AllowUsers deploy

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

# Disable empty passwords
PermitEmptyPasswords no

# Set login grace timeout
LoginGraceTime 30

# Limit max auth attempts
MaxAuthTries 3

# Disable X11 forwarding (unnecessary on servers)
X11Forwarding no

# Disable agent forwarding
AllowAgentForwarding no

# Set client alive interval (auto-disconnect idle sessions)
ClientAliveInterval 300
ClientAliveCountMax 2
EOF
# Restart SSH (keep your current session open in case of issues!)
sudo systemctl restart sshd

⚠️ Critical: Keep your current SSH session open. Open a new terminal and test the new configuration before closing. If you're locked out, you'll need console access through your VPS provider.

2.2 Change SSH Port (Optional but Recommended)

Changing from port 22 to a non-standard port eliminates 95% of automated brute-force attempts:

# Update the Port line in your hardening.conf (shown above)
# Then update your firewall to allow the new port BEFORE restarting SSH

# On your client, connect with custom port:
ssh -p 2222 deploy@your_server_ip

2.3 Use SSH Config for Convenience

# ~/.ssh/config on your LOCAL machine
Host myserver
    HostName your_server_ip
    User deploy
    Port 2222
    IdentityFile ~/.ssh/server_key
    ServerAliveInterval 60

Now you just run ssh myserver.


Phase 3: Firewall Configuration

3.1 UFW (Ubuntu/Debian)

# Install UFW
sudo apt install -y ufw

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

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

# Allow web traffic
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'

# Enable firewall
sudo ufw enable

# Check status
sudo ufw status verbose

3.2 firewalld (RHEL/CentOS)

# Check default zone
sudo firewall-cmd --get-default-zone

# Allow services
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https

# Reload
sudo firewall-cmd --reload

# Verify
sudo firewall-cmd --list-all

3.3 Advanced: Rate Limiting with UFW

# Limit SSH connections (max 6 connections in 30 seconds from same IP)
sudo ufw limit 2222/tcp

# This blocks brute-force attempts at the firewall level

Phase 4: Intrusion Prevention with Fail2Ban

4.1 Install and Configure Fail2Ban

sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
# Ban duration: 1 hour
bantime = 3600
# Time window for counting failures
findtime = 600
# Max failures before ban
maxretry = 3
# Email for alerts (optional)
destemail = your_email@example.com
sender = fail2ban@yourserver.com

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

[recidive]
enabled = true
logpath = /var/log/fail2ban.log
bantime = 604800
findtime = 86400
maxretry = 5
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status

4.2 Monitor Bans

# Check SSH jail status
sudo fail2ban-client status sshd

# Unban an IP (if you locked yourself out)
sudo fail2ban-client set sshd unbanip YOUR_IP

# View fail2ban logs
sudo tail -f /var/log/fail2ban.log

Phase 5: Automatic Security Updates

5.1 Configure Unattended-Upgrades (Debian/Ubuntu)

sudo tee /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF'
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";
};

// Automatically reboot if needed (at a safe time)
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

// Send email about updates
Unattended-Upgrade::Mail "your_email@example.com";
Unattended-Upgrade::MailOnlyOnError "true";
EOF
# Enable automatic updates
sudo systemctl enable --now unattended-upgrades

# Test dry-run
sudo unattended-upgrades --dry-run --verbose

Phase 6: Kernel and System Hardening

6.1 Sysctl Parameters

sudo tee /etc/sysctl.d/99-hardening.conf << 'EOF'
# Disable IP forwarding (unless this is a router)
net.ipv4.ip_forward = 0

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

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

# Enable reverse path filtering
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

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

# Ignore ICMP broadcast requests (Smurf attacks)
net.ipv4.icmp_echo_ignore_broadcasts = 1

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

# TCP SYN cookies (SYN flood protection)
net.ipv4.tcp_syncookies = 1

# Restrict core dumps
fs.suid_dumpable = 0

# Restrict ptrace (prevents process inspection by attackers)
kernel.yama.ptrace_scope = 1

# Limit perf events
kernel.perf_event_paranoid = 2
EOF
sudo sysctl --system

6.2 Disable Unnecessary Services

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

# Common services to disable on a web server
sudo systemctl disable --now avahi-daemon 2>/dev/null
sudo systemctl disable --now cups 2>/dev/null
sudo systemctl disable --now bluetooth 2>/dev/null
sudo systemctl disable --now nfs-common 2>/dev/null
sudo systemctl disable --now rpcbind 2>/dev/null

Phase 7: File System Security

7.1 Secure /tmp

# Make /tmp a tmpfs (RAM-based, noexec, nosuid)
sudo tee -a /etc/fstab << 'EOF'
tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0
EOF
sudo mount -o remount /tmp

7.2 Set Restrictive Permissions

# Restrict cron to root only
sudo chmod 700 /etc/crontab
sudo chmod 700 /etc/cron.hourly
sudo chmod 700 /etc/cron.daily
sudo chmod 700 /etc/cron.weekly
sudo chmod 700 /etc/cron.monthly
sudo chmod 700 /etc/cron.d

# Restrict SSH config
sudo chmod 600 /etc/ssh/sshd_config
sudo chmod 700 /etc/ssh/sshd_config.d

7.3 Enable AIDE (File Integrity Monitoring)

sudo apt install -y aide
sudo aideinit
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# Run integrity check
sudo aide --check

# Set up daily checks
echo "0 5 * * * root /usr/bin/aide --check | mail -s 'AIDE Report' your_email@example.com" | sudo tee /etc/cron.d/aide

Phase 8: Logging and Auditing

8.1 Install auditd

sudo apt install -y auditd audispd-plugins

# Enable process auditing for key commands
sudo auditctl -w /bin/su -p x -k privileged
sudo auditctl -w /bin/sudo -p x -k privileged
sudo auditctl -w /usr/bin/passwd -p x -k privileged

# Make rules persistent
sudo tee /etc/audit/rules.d/hardening.rules << 'EOF'
-w /bin/su -p x -k privileged
-w /bin/sudo -p x -k privileged
-w /usr/bin/passwd -p x -k privileged
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k identity
-w /etc/ssh/sshd_config -p wa -k ssh
-w /var/log/auth.log -p wa -k auth_log
EOF

sudo systemctl enable --now auditd

8.2 Log Rotation

# Ensure logs are rotated (usually pre-configured)
sudo apt install -y logrotate
sudo logrotate -d /etc/logrotate.conf  # dry-run to verify

8.3 Centralized Logging (Optional but Recommended)

For production, forward logs to a centralized system:

# Forward to a log server with rsyslog
sudo tee -a /etc/rsyslog.conf << 'EOF'
*.* @@log-server.example.com:514
EOF
sudo systemctl restart rsyslog

Phase 9: Docker and Container Security

If you're running Docker, add these hardening steps:

9.1 Restrict Docker Socket Access

# Only allow specific users to control Docker
sudo groupadd docker
sudo usermod -aG docker deploy

# Restrict Docker daemon socket
sudo chmod 660 /var/run/docker.sock

9.2 Use User Namespaces

# Enable user namespace remapping
echo '{"userns-remap": "default"}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker

9.3 Scan Images for Vulnerabilities

# Install Trivy for image scanning
sudo apt install -y wget
wget https://github.com/aquasecurity/trivy/releases/latest/download/trivy_*_Linux-64bit.deb
sudo dpkg -i trivy_*_Linux-64bit.deb

# Scan your images before deploying
trivy image nginx:latest

Phase 10: Final Verification Checklist

Run through this list after completing all phases:

  • [ ] System updatedapt upgrade ran, automatic updates enabled
  • [ ] Non-root userdeploy user with sudo access, tested
  • [ ] SSH keys only — password auth disabled, root login disabled
  • [ ] SSH port changed — using non-standard port (e.g., 2222)
  • [ ] Firewall active — only required ports open, rate-limited
  • [ ] Fail2Ban running — SSH jail active, tested with failed login
  • [ ] Automatic updates — unattended-upgrades configured
  • [ ] Kernel hardening — sysctl parameters applied
  • [ ] Unnecessary services disabled — minimal running services
  • [ ] /tmp secured — noexec, nosuid
  • [ ] File integrity monitoring — AIDE initialized
  • [ ] Audit logging — auditd running with rules
  • [ ] Docker hardened — user namespaces, socket restricted (if applicable)
  • [ ] Backups configured — tested restore process

Bonus: Automate with a Hardening Script

Save time on future servers by scripting the whole process:

#!/bin/bash
# harden.sh — Run on a fresh Ubuntu/Debian server
set -euo pipefail

NEW_USER="deploy"
SSH_PORT="2222"

echo "=== Updating system ==="
apt update && apt upgrade -y
apt install -y ufw fail2ban unattended-upgrades aide auditd

echo "=== Creating user ==="
adduser --gecos "" $NEW_USER
usermod -aG sudo $NEW_USER

echo "=== Hardening SSH ==="
cat > /etc/ssh/sshd_config.d/hardening.conf << SSHEOF
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers $NEW_USER
Port $SSH_PORT
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
SSHEOF

echo "=== Configuring firewall ==="
ufw default deny incoming
ufw default allow outgoing
ufw limit $SSH_PORT/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable

echo "=== Configuring fail2ban ==="
cat > /etc/fail2ban/jail.local << F2BEOF
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3

[sshd]
enabled = true
port = $SSH_PORT
maxretry = 3
F2BEOF

echo "=== Enabling auto-updates ==="
echo 'Unattended-Upgrade::Automatic-Reboot "true";' >> /etc/apt/apt.conf.d/50unattended-upgrades

echo "=== Kernel hardening ==="
cat > /etc/sysctl.d/99-hardening.conf << SYSCTLEOF
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
fs.suid_dumpable = 0
kernel.yama.ptrace_scope = 1
SYSCTLEOF
sysctl --system

echo "=== Restarting services ==="
systemctl restart sshd
systemctl enable --now fail2ban

echo "=== HARDENING COMPLETE ==="
echo "Connect with: ssh -p $SSH_PORT $NEW_USER@server_ip"

Conclusion

Server hardening isn't glamorous, but it's the difference between a reliable production environment and a ticking time bomb. Spend one hour on this checklist for every new server, and you'll eliminate the vast majority of automated attacks.

For production environments, also consider:

  • Web Application Firewall (Cloudflare, AWS WAF)
  • DDoS protection (Cloudflare, AWS Shield)
  • SSL/TLS certificate management (Let's Encrypt with auto-renewal)
  • Regular penetration testing
  • SOC2/ISO 27001 compliance (if handling sensitive data)

Security is a process, not a destination. Review and update your hardening procedures quarterly.


Related: Web Security Best Practices, OWASP Top 10 Security Guide, Nginx Configuration Guide.