A new cloud VM is a public IP address with a login prompt on it. That is all it is until you make it something else, and the bots know that better than most admins do. When I left a fresh VPS reachable on port 22 with nothing on it but monitoring, it took 1,001 failed SSH logins on its first day and 28,578 over eleven days, from 658 addresses in 68 countries. Not one of them was aimed at me. The box had no name, no DNS, no service. It had a public IP, and that is enough.
This is the ten minutes that turns that IP into a server. The order is the whole point. Do the provider firewall before you log in for the first time, the SSH settings before you install anything, and the host firewall before you open a port for a service. Do them the other way round and you have built the honeypot first.
Last updated 7 September 2026. Tested on Ubuntu Server 24.04 LTS on Hetzner Cloud and Azure. The commands are the same on Debian 12; the provider steps differ and each has its own section.
Short answer: the ten minutes, in order
Provider firewall first, at create time, default deny, port 22 from your own IP only. Then SSH keys only with password login off. Then a host firewall that also denies by default. Then fail2ban so the guessing gets banned rather than logged. Then automatic security updates. Then check it from outside. Five minutes of it is waiting for apt.
| Minute | Step | Where | How to verify |
|---|---|---|---|
| 0 to 2 | Provider firewall or NSG: deny all inbound, allow 22 from your IP | Cloud console, before first login | Port 22 times out from any other network |
| 2 to 4 | SSH keys only, root password login off | /etc/ssh/sshd_config.d/60-hardening.conf |
sshd -T shows passwordauthentication no |
| 4 to 5 | Host firewall, deny by default | ufw |
ufw status verbose shows deny (incoming) |
| 5 to 7 | fail2ban with the systemd backend | /etc/fail2ban/jail.local |
fail2ban-client status sshd shows a jail, not an error |
| 7 to 9 | Unattended security updates | unattended-upgrades |
/etc/apt/apt.conf.d/20auto-upgrades has both lines set to 1 |
| 9 to 10 | Check from outside | A different network | Only what you allowed answers |
If this is a professional deployment, port 22 should not be on the internet at all. This guide is written for a hobbyist or a lab box: one VM, one admin, one public IP. A production system belongs on a private network with no public SSH, reached through a gateway that exists for the purpose. On Azure that is a VNet with a private-only NIC and Azure Bastion or just-in-time access through Defender for Cloud. On AWS it is a private subnet with Systems Manager Session Manager or an EC2 Instance Connect Endpoint. On Hetzner or any smaller provider it is a private network plus a mesh VPN such as WireGuard, Tailscale or NetBird, so the server has no inbound rule for 22 from anywhere. The ten-minute list below still applies inside that boundary; the difference is that the first step becomes “no public inbound at all”, and the rest is defence in depth rather than the front door.
Minute 0: the provider firewall, before you log in
Every cloud has a firewall that sits outside the VM, in the provider’s network, and it is the only layer that protects you before the operating system is configured. It is also the layer most people set up last, because the console makes it easy not to.
The trap is the same everywhere: the create wizard offers to open SSH for you, and the source it offers is the whole internet. It is a choice you make, not a default you are stuck with, and it is important that “open to everyone” is never the thing you leave in place. Choose your own IP or choose nothing and add the rule afterwards.
Azure: the Network Security Group
A new Azure VM gets an NSG with three built-in inbound rules at low priority: allow traffic from inside the VNet, allow the Azure load balancer probe, and DenyAllInBound at priority 65500. Left alone, nothing from the internet reaches the VM. The exposure comes from the “Inbound port rules” box on the create blade: pick “Allow selected ports” and tick SSH (22), and the portal writes a rule with source Any at priority 300. The portal warns you as it does it. Most people click past the warning.
The correct choices, in order of preference: leave inbound ports at None and add a rule afterwards with source set to My IP address (the portal offers it as a source type); or, if you must allow it at create time, set the source to your IP rather than Any. If your home IP changes, update the rule; do not widen it to Any to save yourself the trouble. For anything beyond a lab box, use Azure Bastion or just-in-time access and have no inbound 22 rule at all. How NSG rules, priorities and service tags fit together is covered in the Azure Virtual Networks and NSGs guide.
Hetzner Cloud: the Cloud Firewall
Hetzner’s firewall is a separate object. A new server with no firewall attached is open on every port, which is exactly the state that produced the numbers at the top of this page. Create the firewall before the server, with one inbound rule: TCP 22 from your IP with a /32 mask, and nothing else. Attach it at create time, either directly or by label so future servers pick it up automatically. Outbound can stay open. The firewall is stateful, so replies to outbound connections come back without a rule.
One thing Hetzner does that Azure does not: with no firewall attached, there is no deny-all backstop. The VM’s own ufw is your only protection until the firewall exists, and ufw is not enabled on a fresh image. That is why the order matters more here than anywhere.
AWS: the security group
The EC2 launch wizard’s default security group suggestion is “Allow SSH traffic from Anywhere (0.0.0.0/0)”. Change the source dropdown to My IP before you launch. A security group is deny-all-inbound by definition, so the only rule it should carry for a lab box is 22 from your address. For anything professional, no SSH rule at all: use Session Manager, which needs no inbound port, or an EC2 Instance Connect Endpoint into a private subnet.
DigitalOcean, Linode, Vultr and the rest
Same shape. Each has a cloud firewall that is not attached by default. Create it with 22 from your IP, attach it to the droplet or instance at create time, and treat “no firewall attached” as “open to the world”, because that is what it means.
Minute 2: SSH keys only, root password login off
Most cloud images already put your key in place and set root to key-only, but not all of them turn off password authentication for other users, and some leave it on through a cloud-init drop-in. Do not trust the image; set it yourself in a drop-in that sorts after cloud-init’s:
sudo tee /etc/ssh/sshd_config.d/60-hardening.conf >/dev/null <<'EOF'
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
EOF
sudo sshd -t && sudo systemctl restart ssh
Ubuntu 24.04 reads sshd_config.d in filename order and the first value wins, so 60- beats cloud-init’s 50-. Verify with the effective config, not the file you edited:
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication) '
Expected: prohibit-password, no, yes. Keep your current session open while you test a new one from a second terminal; if the second one fails, fix it in the first. The eight settings worth going further on, including port changes and per-user restrictions, are in SSH Server Hardening: 8 Settings That Stop Brute-Force Logins.
Minute 4: the host firewall
The provider firewall protects the edge. The host firewall protects the box from its neighbours on the same private network, from services you later bind to the wrong interface, and from your own future mistakes. Deny by default, allow 22 from your IP, enable:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from YOUR.HOME.IP.ADDR to any port 22 proto tcp
sudo ufw enable
sudo ufw status verbose
Two things to know before you rely on it. First, ufw does not protect ports that Docker publishes; Docker inserts its own rules ahead of it. If the box will run containers, the drop has to go in the DOCKER-USER chain, or the container has to bind to 127.0.0.1 and sit behind a reverse proxy. Second, allowing a single IP means a changed home address locks you out; the fix is the provider console, not a wider rule. The full walkthrough, including restricting services to a LAN subnet, is in UFW for Homelabs.
Minute 5: fail2ban, with the setting that makes it work on 24.04
With the provider firewall in place nothing should reach fail2ban at all, so this is the layer for the day the firewall rule gets widened by mistake. Install it, then give it a jail that reads the journal, because on Ubuntu 24.04 there is no /var/log/auth.log by default and a stock install will sit there banning nothing:
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
[DEFAULT]
backend = systemd
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 YOUR.HOME.IP.ADDR
[sshd]
enabled = true
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
The last command should show a jail with a filter and an action list. If it shows an error about the log path, the backend line did not take. Put your own IP in ignoreip or you will ban yourself the first time you mistype a passphrase. Tuning and the lockout trap are in Fail2ban for SSH on Ubuntu.
Minute 7: security updates on a timer
sudo apt update && sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
Both lines in that file should read "1". The default configuration installs security updates only, which is what you want on a server; feature updates stay under your control. Reboots for kernel updates are off by default. Either turn them on with Unattended-Upgrade::Automatic-Reboot "true" and a time, or put a monthly reboot in your calendar. An unrebooted kernel patch is a patch you have not applied.
Minute 9: check it from outside
Every check so far ran on the box. The one that matters runs from somewhere else, because the provider firewall cannot be tested from an address it allows. Use a phone on mobile data, a different VPS, or any network that is not your home:
nc -zv -w 5 YOUR.SERVER.IP 22
nmap -Pn -p 1-1024 YOUR.SERVER.IP
Port 22 should time out rather than refuse, and nothing else should answer. “Refused” means the packet reached the host and the host said no; “timed out” means the provider firewall dropped it, which is what you want. If 22 answers from a network you did not allow, the provider rule is wrong, and nothing you did on the box will save you from what follows.
What you have after ten minutes
A server that answers only your own address on one port, accepts only your key, drops everything else at the provider’s edge and again at the host, bans anything that gets through and tries too often, and patches itself. The 2,600 attempts a day are still out there. They are now a number in a dashboard rather than a queue at your login prompt. Everything you install from here goes behind that boundary, and the seven-step homelab network security checklist picks up where this leaves off: segmentation, containers, admin panels and remote access.
Frequently asked questions
Does a new cloud VM need a firewall if I only use SSH keys?
Yes. Keys stop the guessing from working; they do not stop it from happening, and they do nothing for the next service you install. A fresh VM with key-only SSH still logged roughly 2,600 login attempts a day in my own test. The provider firewall is what turns that into silence, and it also covers ports you have not thought about yet.
Is the Azure NSG open to the internet by default?
No. A new NSG carries a DenyAllInBound rule at the lowest priority, so nothing from the internet reaches the VM until you add an allow rule. The exposure comes from the create wizard’s “Allow selected ports” option, which writes an SSH rule with source Any. Choose None, or set the source to your own IP address, and never leave a rule with source Any in place.
Should I change the SSH port from 22?
It reduces log noise and nothing else. With a provider firewall allowing 22 from your IP only, the scanners never reach the port whichever number it has. Change it if you want a quieter log, keep it on 22 if you want fewer things to remember. Neither replaces the firewall rule.
What is the difference between the provider firewall and ufw?
The provider firewall runs in the cloud network before traffic reaches your VM, so it protects you even when the operating system is misconfigured or not yet configured. ufw runs on the VM itself and protects against neighbours on the private network, services bound to the wrong interface, and mistakes in the provider rule. Use both; they fail differently.
Why is fail2ban not banning anything on Ubuntu 24.04?
Because it is looking for /var/log/auth.log, which Ubuntu 24.04 does not create by default. Set backend = systemd in jail.local so it reads the journal, restart fail2ban, and check fail2ban-client status sshd reports a jail rather than an error.
How should a professional deployment differ from this?
It should have no public SSH port. Put the VM on a private network with a private-only NIC and reach it through a gateway built for the job: Azure Bastion or just-in-time access on Azure, Session Manager or an EC2 Instance Connect Endpoint on AWS, a mesh VPN such as WireGuard, Tailscale or NetBird elsewhere. The hardening on the box still applies as defence in depth.
RTFM Apparel · printed in the UK

ReadTheManual is run, written and curated by Eric Lonsdale.
Eric has over 20 years of professional experience in IT infrastructure, cloud architecture, and cybersecurity, but started with PCs long before that.
He built his first machine from parts bought off tables at the local college campus, hoping they worked. He learned on BBC Micros and Atari units in the early 90s, and has built almost every PC he’s used between 1995 and now.
From helpdesk to infrastructure architect, Eric has worked across enterprise datacentres, Azure environments, and security operations. He’s managed teams, trained engineers, and spent two decades solving the problems this site teaches you to solve.
ReadTheManual exists because Eric believes the best way to learn IT is to build things, break things, and actually read the manual. Every guide on this site runs on infrastructure he owns and maintains.
Enjoyed this guide?
New articles on Linux, homelab, cloud, and automation every 2 days. No spam, unsubscribe anytime.





