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:
| Metric | Tool | Alert threshold |
|---|---|---|
| Disk usage | df -h | > 85% |
| Load average | uptime | > CPU count for 5 min |
| Memory | free -h | < 10% available |
| Service status | systemctl | Any stopped service |
| SSH failures | journalctl | Spike in failed auth |
| External uptime | UptimeRobot or similar | Any 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:
- Take a provider snapshot (DigitalOcean, Hetzner, Vultr, Linode all support this)
- Name it descriptively:
ubuntu-24.04-hardened-baseline-YYYYMMDD - 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.