Security

Network Hardening: Monitoring for Unauthorized Port Scans on the VPS

Learn how to monitor port scans on a VPS, log the source addresses that probe your server, and block repeat offenders automatically on Linux and Windows.

Intermediate6 min read

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 run Get-NetTCPConnection -State Listen in 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:

Bash
sudo nft -a list chain inet filter input

You 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:

Bash
sudo nft insert rule inet filter input handle 12 \
  limit rate 10/minute log prefix "DROP-IN: " level warn

If your ruleset has no drop rule yet, add a log rule at the end of the chain instead:

Bash
sudo nft add rule inet filter input \
  limit rate 10/minute log prefix "DROP-IN: " level warn

Read the results from the kernel log:

Bash
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:

INI
# /etc/fail2ban/filter.d/portscan.conf
[Definition]
failregex = ^DROP-IN: .*SRC=<HOST> .*
ignoreregex =

Then add a jail:

INI
# /etc/fail2ban/jail.d/portscan.local
[portscan]
enabled  = true
filter   = portscan
logpath  = /var/log/kern.log
maxretry = 5
findtime = 60
bantime  = 3600

If 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:

Bash
sudo fail2ban-regex /var/log/kern.log /etc/fail2ban/filter.d/portscan.conf

Then reload:

Bash
sudo systemctl restart fail2ban
sudo fail2ban-client status portscan

Warning: 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:

Bash
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:

Bash
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 | head

One 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:

PowerShell
Set-NetFirewallProfile -Profile Domain,Public,Private `
  -LogBlocked True -LogAllowed False -LogMaxSizeKilobytes 32767 `
  -LogFileName '%systemroot%\system32\LogFiles\Firewall\pfirewall.log'

Confirm the settings stuck:

PowerShell
Get-NetFirewallProfile | Select-Object Name, LogBlocked, LogFileName

Read the newest entries:

PowerShell
Get-Content "$env:systemroot\system32\LogFiles\Firewall\pfirewall.log" -Tail 50

Group blocked drops by source address to spot a scanner:

PowerShell
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 10

For 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:

Bash
nmap -Pn -p 1-1000 203.0.113.10

Then check the server side. On Linux:

Bash
sudo journalctl -k --since "5 minutes ago" | grep "DROP-IN" | tail -20
sudo fail2ban-client status portscan

On 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 accept rule, or the kernel log is not written to a file. Check with sudo nft -a list chain inet filter input, and read from journalctl -k if /var/log/kern.log is 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 failregex does not match your log format. Run sudo fail2ban-regex against the real log file and adjust the pattern until it reports matches.
  • You banned yourself. Add your IP to ignoreip, then sudo 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-NetFirewallProfile and confirm the LogFiles\Firewall folder exists.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.