Overview

A fresh Hostinger VPS ships with root SSH enabled and password auth on. That is the default attack surface for automated credential scanners. Run every step on this page before installing any app. Hardening takes less than ten minutes and eliminates the most common classes of initial compromise. See hostinger-vps for the broader deployment picture.

Create a non-root deploy user first

Never run your app as root and never leave root as the only SSH target.

# Run as root on first login
adduser deploy
usermod -aG sudo deploy
# Copy your local key into the new user's authorized_keys
mkdir -p /home/deploy/.ssh
cp ~/.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

Open a second terminal and confirm you can SSH as deploy before locking out root. Never lock yourself out by testing changes only in the active session.

Disable root SSH login and password authentication

After confirming key-based deploy login works, edit /etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no

Then reload the daemon:

systemctl reload ssh

Key-only auth removes the brute-force surface entirely. A scanner that cannot attempt passwords has nothing to guess. Disabling root login means even a stolen key for root grants nothing; the attacker still needs to escalate from a real user account.

Configure UFW to deny by default

UFW wraps iptables and makes firewall state auditable with a single command.

ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH       # port 22
ufw allow 80/tcp        # HTTP
ufw allow 443/tcp       # HTTPS
ufw enable
ufw status verbose

Add rules before enabling, not after, or you may lose your SSH session. The rule order matters: allow OpenSSH before enable ensures the current connection stays open.

Revisit open ports after every new service. Run ufw status numbered to audit the list. Remove stale rules with ufw delete <number>.

Install and configure fail2ban

fail2ban watches log files and bans IPs that exceed a threshold of failed attempts.

apt install -y fail2ban
systemctl enable --now fail2ban

The default jail for SSH activates automatically. Verify it:

fail2ban-client status sshd

Override defaults in /etc/fail2ban/jail.local rather than jail.conf; the .local file survives package upgrades:

[sshd]
enabled  = true
maxretry = 5
bantime  = 3600
findtime = 600

This bans any IP that fails five times in ten minutes for one hour. Increase bantime to 86400 on servers that see heavy scan traffic.

Enable unattended security upgrades

Unpatched packages are the second most common entry point after weak credentials.

apt install -y unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades

Confirm the config in /etc/apt/apt.conf.d/50unattended-upgrades. Ensure Unattended-Upgrade::Allowed-Origins includes the security pocket:

"${distro_id}:${distro_codename}-security";

Enable automatic reboots for kernel updates during a low-traffic window:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

Reboot windows apply only to kernel and libc updates. Most security patches apply without a reboot.

Verify the hardened state before going live

Run through this checklist after hardening:

# Confirm root login blocked
ssh root@<ip>           # Should reject
 
# Confirm password auth blocked
ssh -o PreferredAuthentications=password deploy@<ip>  # Should reject
 
# Confirm firewall
ufw status verbose      # Only 22, 80, 443 open
 
# Confirm fail2ban running
fail2ban-client status  # sshd jail active
 
# Confirm upgrades
unattended-upgrade --dry-run --debug 2>&1 | head -20

Document the date and checklist result. Repeat the audit after any major package or config change.