A port scan is the reconnaissance step that comes before almost every attack: someone sweeps your VPS looking for SSH, RDP, database or panel ports. This guide shows you how to monitor port scans on a VPS, log the source addresses that probe you, and block repeat offenders automatically on both Linux and Windows.
Before you start
You need:
- Root or sudo access on a Linux VPS, or Administrator rights on a Windows VPS or Admin RDP session.
- A way in: SSH on Linux, or Remote Desktop on Windows. Windows servers accept RDP on TCP port 3389, so you connect to
203.0.113.10:3389. - A second machine you control, so you can scan your own server and confirm the logs fill up.
- A list of the ports you actually serve. On Linux run
sudo ss -tulpn. On Windows runGet-NetTCPConnection -State Listenin an elevated PowerShell window.
Warning: Firewall changes can lock you out. Keep your current SSH or RDP session open, make changes from there, and test from a second session before you disconnect.
Know what a scan looks like
A scanner sends packets to ports where nothing is listening, or to ports that are open but should not be. The three patterns worth alerting on:
- Many connection attempts from one source IP in a short window.
- SYNs to ports you do not serve, which your firewall drops.
- Repeated half-open connections that never complete a handshake.
Your firewall already drops that traffic. The goal here is to make it visible, so you can tell a scan from normal user traffic and act on it.
Monitor port scans with nftables logging
Most current Linux distributions use nftables, even when you manage rules through UFW. First look at the input chain and note the handle of the final drop rule:
sudo nft -a list chain inet filter inputYou will see something like counter packets 0 bytes 0 drop # handle 12. Insert a rate-limited log rule just before that handle so dropped packets are recorded without flooding your disk:
sudo nft insert rule inet filter input handle 12 \
limit rate 10/minute log prefix "DROP-IN: " level warnIf your ruleset has no drop rule yet, add a log rule at the end of the chain instead:
sudo nft add rule inet filter input \
limit rate 10/minute log prefix "DROP-IN: " level warnRead the results from the kernel log:
sudo journalctl -k --since "1 hour ago" | grep "DROP-IN"On Debian and Ubuntu with rsyslog installed, the same lines also land in /var/log/kern.log. The SRC= field is the scanner's address and DPT= is the port they tried.
Tip: If you use iptables instead, insert a matching rule with sudo iptables -I INPUT 5 -m limit --limit 5/min -j LOG --log-prefix "PORTSCAN: " --log-level 4, then read it with sudo journalctl -k | grep PORTSCAN.
Block repeat scanners with Fail2ban
Logging tells you what happened. Fail2ban turns those log lines into bans. Create a filter that matches the prefix you just configured:
# /etc/fail2ban/filter.d/portscan.conf
[Definition]
failregex = ^DROP-IN: .*SRC=<HOST> .*
ignoreregex =Then add a jail:
# /etc/fail2ban/jail.d/portscan.local
[portscan]
enabled = true
filter = portscan
logpath = /var/log/kern.log
maxretry = 5
findtime = 60
bantime = 3600If your server logs only to the systemd journal, drop the logpath line and add backend = systemd instead. Test the filter against real log lines before you rely on it:
sudo fail2ban-regex /var/log/kern.log /etc/fail2ban/filter.d/portscan.confThen reload:
sudo systemctl restart fail2ban
sudo fail2ban-client status portscanWarning: Add your own address to ignoreip in /etc/fail2ban/jail.local before enabling this jail. A mis-tuned filter can ban you mid-session.
Watch live traffic with tcpdump
When you want to see a scan as it happens, capture SYN packets that are not part of an established connection:
sudo tcpdump -nn -i eth0 -c 50 \
'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'To rank the noisiest sources, capture a batch and count them:
sudo tcpdump -nn -i eth0 -c 500 'tcp[tcpflags] & tcp-syn != 0' 2>/dev/null \
| awk '{print $3}' | cut -d. -f1-4 | sort | uniq -c | sort -rn | headOne or two addresses with hundreds of hits in a few seconds is a scan. A steady trickle from many addresses is usually internet background noise.
Monitor port scans on Windows Server
Windows Firewall can log dropped packets. Turn logging on for every profile:
Set-NetFirewallProfile -Profile Domain,Public,Private `
-LogBlocked True -LogAllowed False -LogMaxSizeKilobytes 32767 `
-LogFileName '%systemroot%\system32\LogFiles\Firewall\pfirewall.log'Confirm the settings stuck:
Get-NetFirewallProfile | Select-Object Name, LogBlocked, LogFileNameRead the newest entries:
Get-Content "$env:systemroot\system32\LogFiles\Firewall\pfirewall.log" -Tail 50Group blocked drops by source address to spot a scanner:
Select-String -Path "$env:systemroot\system32\LogFiles\Firewall\pfirewall.log" -Pattern 'DROP' |
ForEach-Object { ($_ -split ' ')[4] } |
Group-Object | Sort-Object Count -Descending | Select-Object -First 10For RDP specifically, the cleanest hardening is to stop scanners reaching port 3389 at all. Restrict it to your own addresses, or tunnel it over SSH.
Check it works
From a second machine you control, scan a small port range:
nmap -Pn -p 1-1000 203.0.113.10Then check the server side. On Linux:
sudo journalctl -k --since "5 minutes ago" | grep "DROP-IN" | tail -20
sudo fail2ban-client status portscanOn Windows, re-run the Select-String command above and look for new DROP lines with your test machine's address. You should see the source IP, the destination ports you scanned, and — once the retry threshold is reached — a ban in Fail2ban's status output.
Troubleshooting
- No log lines at all. The log rule sits after an
acceptrule, or the kernel log is not written to a file. Check withsudo nft -a list chain inet filter input, and read fromjournalctl -kif/var/log/kern.logis missing. - Logs grow fast and fill the disk. Remove the rate limit you added, or lower it to
limit rate 3/minute. Never log every packet on a busy server. - Fail2ban reports the jail as active but bans nobody. The
failregexdoes not match your log format. Runsudo fail2ban-regexagainst the real log file and adjust the pattern until it reports matches. - You banned yourself. Add your IP to
ignoreip, thensudo fail2ban-client set portscan unbanip 203.0.113.10. - Windows firewall log stays empty. Logging is off for the active profile, or the file path is not writable. Re-check
Get-NetFirewallProfileand confirm theLogFiles\Firewallfolder exists.
Next steps
- Tune noisy jails with Fail2ban custom jails and log filters.
- Add rate limiting at the firewall with Advanced UFW rate limiting.
- Build stateful filter chains in nftables rule chaining.
- On Windows, lock down remote access with Restrict RDP port 3389 to your own IP addresses.
- Compare plan network specs in Port speed vs bandwidth.
