A fresh VPS is exposed to the internet within minutes of creation. Before installing WordPress, a control panel, or any application, lock down access and make sure you can recover from mistakes. This checklist covers the first 30 minutes after provisioning.

Step 1: Update the system

Start from a known state:

apt update && apt upgrade -y

Check the release:

lsb_release -a
uname -r

Avoid Ubuntu 24.10 (non-LTS, short support window) for production servers. Use 24.04 LTS or the current LTS.

Step 2: Create a non-root user

adduser deploy
usermod -aG sudo deploy

Copy your SSH key to the new user:

mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

Test the new user in a separate terminal before restricting root.

Step 3: Harden SSH

Edit /etc/ssh/sshd_config:

# Disable root login
PermitRootLogin no

# Only allow key-based authentication
PubkeyAuthentication yes
PasswordAuthentication no

# Only allow specific users
AllowUsers deploy

# Limit authentication attempts
MaxAuthTries 3
MaxSessions 5

# Disable empty passwords and challenge-response
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes

# Stronger key exchange and ciphers (optional, may break older clients)
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com

Before reloading, test the configuration:

sshd -t

If the test passes, reload:

systemctl reload sshd

Critical: Open a second terminal and verify you can log in as the new user before closing any existing root session. If SSH is misconfigured and your current session ends, you will be locked out.

Step 4: Enable the firewall

ufw default deny incoming
ufw default allow outgoing

# Essential services
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp

# If using a control panel, add its ports explicitly
# ufw allow 2222/tcp   # Example: custom SSH port
# ufw allow 8090/tcp   # Example: admin panel

ufw enable
ufw status verbose

Never leave UFW disabled. Every minute a new VPS sits with no firewall, automated scanners are probing it.

Step 5: Enable automatic security updates

apt install unattended-upgrades
dpkg-reconfigure unattended-upgrades

Configure /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";
};

Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";

Set Automatic-Reboot "false" unless you have monitoring and maintenance windows. Kernel updates without a reboot do nothing — but you want to control when the reboot happens.

Step 6: Install fail2ban

Fail2ban reduces brute-force noise. It is not a complete security strategy, but it blocks the automated scripts that scan SSH, WordPress login pages, and common attack paths:

apt install fail2ban
systemctl enable --now fail2ban
fail2ban-client status

Create a local jail configuration for SSH:

# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5

[sshd]
enabled = true
mode = aggressive

For WordPress if the server hosts it:

[wordpress]
enabled = true
filter = wordpress
logpath = /var/log/nginx/access.log
maxretry = 10

Step 7: Basic system tuning

Timezone and NTP

timedatectl set-timezone UTC
timedatectl set-ntp true

UTC is the standard for servers. All logs should be in UTC to avoid timezone confusion during incidents.

Swappiness

Reduce swappiness on servers with adequate RAM:

echo 'vm.swappiness=10' >> /etc/sysctl.d/99-crondaily.conf
sysctl -p /etc/sysctl.d/99-crondaily.conf

Open file limits

For web servers and databases:

echo 'fs.file-max = 65535' >> /etc/sysctl.d/99-crondaily.conf
echo '* soft nofile 65535' >> /etc/security/limits.conf
echo '* hard nofile 65535' >> /etc/security/limits.conf

Step 8: Configure basic monitoring

At minimum, set up monitoring for:

MetricToolAlert threshold
Disk usagedf -h> 85%
Load averageuptime> CPU count for 5 min
Memoryfree -h< 10% available
Service statussystemctlAny stopped service
SSH failuresjournalctlSpike in failed auth
External uptimeUptimeRobot or similarAny downtime

A simple disk monitor script:

#!/bin/bash
THRESHOLD=85
USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$USAGE" -gt "$THRESHOLD" ]; then
  echo "Disk at ${USAGE}% on $(hostname)" | mail -s "DISK ALERT" admin@example.com
fi

Step 9: Take a snapshot

Before installing any application stack:

  1. Take a provider snapshot (DigitalOcean, Hetzner, Vultr, Linode all support this)
  2. Name it descriptively: ubuntu-24.04-hardened-baseline-YYYYMMDD
  3. Test that you can create a new server from the snapshot

If the application install goes wrong or a security change breaks things, you can return to a clean, secured baseline in minutes.

Step 10: Document the baseline

Record:

  • SSH port and allowed users
  • Firewall rules
  • Fail2ban jails
  • Monitoring endpoints and thresholds
  • Snapshot name and date
  • Any non-default configurations

Future you — or the next person who touches this server — should be able to audit security in minutes, not hours. A hardened server without documentation becomes a liability the moment you forget which rules were intentional.