A fresh VM with a public IP starts receiving SSH login attempts within minutes. Almost all of it is opportunistic: automated sweeps looking for default credentials, exposed services and unpatched software. It is not targeted at you and it does not need to be — the scanners work through the address space and try everything.
This baseline takes about half an hour and removes essentially all of that class of attack. It is not a substitute for a security programme. It is the floor.
Password authentication on a public SSH port is the single largest exposure on a default install. Copy your key up, verify you can log in with it, then turn passwords off.
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@your-server
# verify in a SECOND terminal before continuing
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
PasswordAuthentication no
PermitRootLogin no
KbdInteractiveAuthentication no
MaxAuthTries 3
AllowUsers deploy
EOF
sudo sshd -t && sudo systemctl reload ssh
Keep the second terminal open until you have confirmed a fresh login works. Locking yourself out of a cloud VM is recoverable through the console, but it is an unpleasant way to spend an afternoon.
Changing the SSH port is optional and mildly useful — it removes you from the noisiest scans and cleans up your logs. It is not a security control; treat it as noise reduction.
Close everything, then open what the machine actually serves. Do this at the provider's network firewall as well as on the host — the host firewall protects against misconfiguration, the network one protects against the host being wrong.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80,443/tcp
sudo ufw enable
sudo ufw status verbose
Then check what is actually listening, because it is rarely only what you expect:
sudo ss -tulpn | grep LISTEN
Databases and caches bound to 0.0.0.0 are the classic finding. Bind them to the private interface or to localhost, and do not rely on the firewall alone to save you.
The vulnerabilities used in opportunistic attacks are almost always old and patched. Unattended upgrades are the highest-value thirty seconds in this list.
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# confirm it is actually running
systemctl status unattended-upgrades
cat /var/log/unattended-upgrades/unattended-upgrades.log
Enable automatic reboots for kernel updates on anything where you can tolerate a restart window, and schedule it for a quiet hour. A server that installs kernel patches and never reboots is running the vulnerable kernel with a reassuring log file.
With keys-only SSH, brute force cannot succeed — but the attempts still consume resources and bury real events in your logs.
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
EOF
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
If the machine runs a web application, add a jail for repeated 401s and 404s against it. The same infrastructure that stops SSH scanning stops credential stuffing.
Applications should not run as root and neither should you. One unprivileged user per human, plus a service account per application, each with only the sudo rights it needs.
sudo adduser --disabled-password --gecos "" deploy
sudo usermod -aG sudo deploy
# service account with no login shell
sudo adduser --system --group --no-create-home --shell /usr/sbin/nologin appsvc
Hardening without monitoring means you find out about the breach from a third party. Minimum viable visibility:
None of it is clever, all of it is checkable, and it eliminates the attacks that make up the overwhelming majority of what a public IP receives. Anything beyond this — intrusion detection, mandatory access control, formal hardening benchmarks — is worth doing on machines that warrant it, but not before the six above are done everywhere.