Detecting Nmap Scans: The View From the Defender's Side

The site already teaches you how to run every nmap scan. This one flips the camera around: what a SYN scan, a UDP scan and a ping sweep actually look like in an IOS XE ACL counter, in syslog and in a packet capture, plus why a slow scan beats most of it.

Terminal card showing an nmap SYN scan alongside the Cisco ACL hit counters and rate-limit syslog it produced

Most nmap writing, including most of what is on this site, is written from behind the keyboard that launches the scan. You learn which flag does what, how to read open versus filtered, and how to get through a firewall that would rather you did not. That is half the picture. The other half is what your routers, your switches and your packet captures record while all of that is happening, and whether anyone is actually looking. If you want the attacker-side material first, the guide to running nmap scans against network gear covers it in depth.

This article turns the camera around. Every scan below was run for real from a Kali box, and every detection artefact below was pulled off the target device and off the wire immediately afterwards. The point is not to sell you an IDS. The point is that a plain Cisco router, with no extra licensing and no sensor appliance, already records enough to spot a port scan, and that most engineers never look at that data because they think ACLs are only for blocking.

Captures come from a CML lab: Kali running nmap 7.99 at 192.168.99.101, a target router R2 at 192.168.99.2 (iol-xe, IOS XE 17.18.2) with a logging ACL inbound on Et0/0, and a Debian VM at 192.168.99.100 running tcpdump on the same segment. Everything was captured on 2026-07-24.

The lab behind these captures

Three machines on one broadcast domain, which is deliberate. A scan crossing a firewall gets sanitised on the way; on the local segment you see the raw behaviour, including the ARP that a -sn sweep leans on. Kali is bridged into CML through the external connector, so this is a real Linux host attacking a real IOS XE image.

AttackerKali 192.168.99.101 (eth1), nmap 7.99
Target and sensor 1R2 192.168.99.2, iol-xe, IOS XE 17.18.2
Sensor 2Debian VM 192.168.99.100, tcpdump on ens224
Detection configACL SCAN-DETECT applied inbound on Et0/0
Scan window27 seconds, four scan types

From the attacker's seat, this is boring

Start with what the operator sees, because the contrast is the whole argument. Two hundred TCP ports, half-open SYN scan, with --reason so nmap tells you why it decided what it decided:

$ sudo nmap -sS -p 1-200 --reason 192.168.99.2

Starting Nmap 7.99 ( https://nmap.org ) at 2026-07-24 16:01 -0700
Nmap scan report for 192.168.99.2
Host is up, received arp-response (0.0055s latency).
Not shown: 199 closed tcp ports (reset)
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 255
MAC Address: AA:BB:CC:00:DC:00 (Unknown)
Nmap done: 1 IP address (1 host up) scanned in 4.05 seconds

Four seconds. One open port. No error, no timeout, no sign that anything unusual happened. A UDP scan of four common ports came back just as fast, every port closed because R2 answered each probe with an ICMP port-unreachable at TTL 255. A -sn sweep of the whole /24 found six live hosts in 2.93 seconds. An -O fingerprint guessed "Cisco 1921 router (IOS 15.1)" at 96 percent, wrong in the details and right about the thing that matters. The mechanics of why a lone SYN is enough to tell an open port from a closed one are a separate story; the point here is that all four scans cost 27 seconds of wall clock and produced nothing that would make an attacker nervous.

Sensor one: the ACL you already have

The detection surface on R2 is not a feature you buy. It is the log keyword on an ACE, and the key insight is that log works on a permit as happily as it does on a deny. That turns an access list from a filter into an instrument. Traffic passes normally, but the first packet of each flow generates a %SEC-6-IPACCESSLOGP message and the ACE hit counter increments for every packet.

ip access-list extended SCAN-DETECT
 permit tcp any any eq 22 log
 permit tcp any host 192.168.99.2 log
 permit udp any any log
 permit icmp any any log
 permit ip any any
!
interface Ethernet0/0
 ip access-group SCAN-DETECT in
!
service timestamps log datetime msec
logging buffered 200000 debugging

The final permit ip any any with no log is the escape valve: everything uninteresting still passes without filling your buffer. Now read the counters after the scan window:

R2# show ip access-lists SCAN-DETECT
Extended IP access list SCAN-DETECT
    10 permit tcp any any eq 22 log (257 matches)
    20 permit tcp any any eq telnet log (2 matches)
    30 permit tcp any any eq www log (2 matches)
    40 permit tcp any any eq 443 log (1 match)
    50 permit tcp any host 192.168.99.2 log (1420 matches)   <-- the sweep
    60 permit udp any any log (26 matches)
    70 permit icmp any any log (10 matches)
    80 permit ip any any (17 matches)

That is the fingerprint, and you can see it without a single extra tool. Line 50, the catch-all for anything aimed at R2's own address, absorbed 1420 hits. The named-service lines that represent real traffic saw single or double digits: two telnet, two HTTP, one HTTPS. Normal traffic to a router is lopsided in the opposite direction, concentrated on the handful of ports that actually run services. When your generic line outweighs your service lines by two orders of magnitude, something enumerated you.

The UDP and ICMP counters tell the rest of the story. Twenty six UDP hits covers the four-port -sU scan plus background broadcast noise, and the ten ICMP hits are the discovery probes from the -sn sweep hitting R2's interface. Different nmap scan types land on different ACEs, which means a well-structured ACL gives you a crude protocol breakdown of the scan for free.

The syslog fan-out: one source, many destination ports

Counters tell you something happened. Syslog tells you who and what. This is show logging filtered to the ACL messages, trimmed to the interesting stretch:

R2# show logging | include IPACCESSLOG
*Jul 24 23:01:26.597: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted tcp 192.168.99.101(51831) -> 192.168.99.2(199), 1 packet
*Jul 24 23:01:27.708: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted tcp 192.168.99.101(51833) -> 192.168.99.2(85), 1 packet
*Jul 24 23:01:28.814: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted tcp 192.168.99.101(51835) -> 192.168.99.2(65), 1 packet
*Jul 24 23:01:29.920: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted tcp 192.168.99.101(51837) -> 192.168.99.2(79), 1 packet
*Jul 24 23:01:31.968: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted udp 192.168.99.101(47800) -> 192.168.99.2(123), 1 packet
*Jul 24 23:01:33.073: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted udp 192.168.99.101(47802) -> 192.168.99.2(67), 1 packet
*Jul 24 23:01:34.178: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted udp 192.168.99.101(47804) -> 192.168.99.2(161), 1 packet
*Jul 24 23:01:38.190: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted tcp 192.168.99.101(46826) -> 192.168.99.2(3306), 1 packet
*Jul 24 23:01:39.312: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted tcp 192.168.99.101(46828) -> 192.168.99.2(10010), 1 packet
*Jul 24 23:01:40.417: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted tcp 192.168.99.101(46830) -> 192.168.99.2(9101), 1 packet

Read the destination ports in order: 199, 85, 65, 79, then 3306, 10010, 9101. There is no application on earth whose client talks to that set of ports in eleven seconds. Legitimate traffic converges on a small number of destination ports and spreads across many source ports; a scan does the opposite, marching through destination ports while the source port ticks up by two each time. That inversion is the single most reliable behavioural signature of a scan, and it is what every commercial detection engine is ultimately measuring, whatever they call it in the datasheet.

Note that 1 packet closes every line. A normal flow eventually logs a higher count as it continues. One packet per flow, hundreds of flows, one source, is enumeration, not communication.

The rate-limit line nobody reads

Buried in the same output is a message that is easy to dismiss as housekeeping noise. It is not.

*Jul 24 23:01:41.434: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted tcp 192.168.99.101(58846) -> 192.168.99.2(22), 1 packet
*Jul 24 23:01:41.920: %SEC-6-IPACCESSLOGRL: access-list logging rate-limited or missed 1423 packets
*Jul 24 23:01:43.146: %SEC-6-IPACCESSLOGP: list SCAN-DETECT permitted tcp 192.168.99.101(59098) -> 192.168.99.2(22), 1 packet

IOS caps how fast it will generate ACL log messages, because building a syslog message is a process-switched operation and an attacker who can make you log fast enough can hurt the control plane. When the cap is hit, IOS emits %SEC-6-IPACCESSLOGRL and tells you how many packets it gave up on. Here that number is 1423, which is essentially the whole scan.

Two conclusions follow, and they pull in opposite directions. The first is that IPACCESSLOGRL is itself a high-quality detection signal. Nothing in a healthy network generates 1423 new flows in a burst against one router. If that message appears in your syslog server and you have no maintenance window running, you have your alert (arguably a better one than any individual IPACCESSLOGP line, because it is a single unambiguous event rather than a pattern you have to correlate).

The second is more sobering. Syslog undercounted the scan by more than a thousand packets: the ACE counter said 1420 hits, syslog showed roughly twenty messages. If your detection rule says "alert when I see fifty scan-like flows from one source", the box will never send you fifty. That is the honest argument for NetFlow or a proper IDS on top of ACL logging. ACL logging tells you a scan happened; it will not tell you how big it was.

The scan on the wire

The router's view is a summary. The capture is ground truth, and it is where the scan type becomes unmistakable. This is tcpdump on the Debian VM, filtered to the attacker, while a fresh -sS ran from Kali:

$ sudo tcpdump -i ens224 -nn -c 40 -w res0030-synscan.pcap 'tcp and host 192.168.99.101' -Z root
tcpdump: listening on ens224, link-type EN10MB (Ethernet), snapshot length 262144 bytes
40 packets captured
201 packets received by filter
0 packets dropped by kernel

$ tcpdump -nn -r res0030-synscan.pcap | head -20
16:03:10.968095 IP 192.168.99.101.47066 > 192.168.99.2.53: Flags [S], seq 3251516137, win 1024, options [mss 1460], length 0
16:03:10.968140 IP 192.168.99.101.47066 > 192.168.99.2.111: Flags [S], seq 3251516137, win 1024, options [mss 1460], length 0
16:03:10.968231 IP 192.168.99.101.47066 > 192.168.99.2.21: Flags [S], seq 3251516137, win 1024, options [mss 1460], length 0
16:03:10.968271 IP 192.168.99.101.47066 > 192.168.99.2.22: Flags [S], seq 3251516137, win 1024, options [mss 1460], length 0
16:03:10.968306 IP 192.168.99.101.47066 > 192.168.99.2.80: Flags [S], seq 3251516137, win 1024, options [mss 1460], length 0
16:03:10.972170 IP 192.168.99.2.53 > 192.168.99.101.47066: Flags [R.], seq 0, ack 3251516138, win 0, length 0
16:03:10.972248 IP 192.168.99.2.111 > 192.168.99.101.47066: Flags [R.], seq 0, ack 3251516138, win 0, length 0
16:03:10.972554 IP 192.168.99.2.21 > 192.168.99.101.47066: Flags [R.], seq 0, ack 3251516138, win 0, length 0
16:03:10.973236 IP 192.168.99.2.22 > 192.168.99.101.47066: Flags [S.], seq 3976364105, ack 3251516138, win 65535, options [mss 1460,sackOK,eol], length 0
16:03:10.973355 IP 192.168.99.101.47066 > 192.168.99.2.22: Flags [R], seq 3251516138, win 0, length 0   <-- half-open teardown

Five things in that capture identify a SYN scan beyond argument. All the probes come from one source port (47066) and carry an identical sequence number, because nmap is not tracking state per flow. Every probe is a bare [S] with length 0 and a tiny 1024-byte window. The destination ports jump around with no application logic. Closed ports answer [R.], so a wall of RSTs back to one host is as diagnostic as the SYNs going out. And the one port that answers [S.], port 22, immediately gets an [R] from the attacker instead of the ACK that would complete the handshake. That refusal to finish the handshake is what makes -sS half-open, and it is why the scan never appears in the SSH daemon's own logs.

Counting unique destination ports from that source in the 40-frame capture gives 29. Twenty nine destinations from one source in a fraction of a second is not a threshold anyone needs to argue about. If you are not comfortable reading flag notation at speed, the practical tcpdump guide for network engineers covers the filter syntax, and there is a broader walkthrough of reading a capture to find the anomaly in the packet analysis material.

Signatures by scan type

Here is how the four scans run in this lab map to what a defender can observe. Everything in this card was seen in the captures above.

-sS SYN scan
On the wireLone [S], one source port, no ACK
On the routerGeneric TCP ACE spikes (1420)
Tell[R] back after a [S.]
-sU UDP scan
On the wireICMP port-unreach per closed port
On the routerUDP ACE, 26 matches
Telludp flows to 53, 67, 123, 161
-sn ping sweep
On the wireARP on-segment, ICMP off-segment
On the routerICMP ACE, 10 matches
Tell256 addresses probed, 6 answered
-O fingerprint
On the wire1000 ports plus odd probe flags
On the routerLargest counter jump of the four
Tell999 closed ports plus a UDP probe

The slow scan problem, honestly

Everything above assumes default timing. Nmap ran at its normal pace, dumped 1420 packets at one router in a few seconds, and lit up every counter on the box. Change one flag and most of that goes away.

Timing templates from -T0 through -T5 control probe parallelism and inter-probe delay, and the slow end of that range exists specifically to fall under detection thresholds; the article on tuning nmap timing and parallelism covers what each one changes. This lab did not capture a slow scan, so treat what follows as reasoning rather than measurement.

The ACE hit counter survives. It is cumulative and it does not care whether 1420 packets arrived in four seconds or four days. If you baseline show ip access-lists weekly and diff it, a slow scan still shows up as a generic-line counter that grew when nothing else did. That is genuinely one of the few cheap defences against slow enumeration, and almost nobody does it.

The packet-rate signals do not survive. %SEC-6-IPACCESSLOGRL never fires, because a probe every few seconds never outruns the logger. The per-flow messages still appear, but scattered across days of unrelated log volume, which turns finding them into a correlation problem rather than a reading problem. The tcpdump signature is technically identical (same lone SYNs, same half-open teardown) but you would never catch it in a 40-frame capture.

Be honest with yourself about this. A patient attacker probing twenty ports a day from a residential IP is not going to be caught by an ACL counter and a tired engineer. Catching that requires stateful flow records with long retention and something doing the grouping for you. What the router gives you for free is excellent coverage of the loud, fast, automated scanning that makes up the overwhelming majority of what actually hits your perimeter.

The arms race runs both directions

Notice that every detection above keys on the source IP address. That is the weak joint, and attackers know it. Decoy scanning (-D) interleaves your real probes with probes bearing forged source addresses, so the defender's log fills with a dozen plausible-looking scanners and no way to tell which one is holding the keyboard. Fragmentation, source-port spoofing and idle scanning attack the same assumption from other angles; the full catalogue is in the piece on getting a scan past a firewall undetected.

The same %SEC-6-IPACCESSLOG surface has been captured recording forged source addresses generated with Scapy, and it records them exactly as faithfully as it records real ones, because the ACL is reading a header field and the header field is a lie. That is a boundary on the technique, not a flaw in it. Source-based logging tells you enumeration is happening on your network; it does not reliably tell you who is doing it. That second question needs correlation the router cannot do alone: switch port mapping, upstream flow records, or a sensor that tracks conversation state.

Gotchas

  • Counters and syslog will disagree, and syslog is the liar. The ACE showed 1420 matches while syslog showed about twenty messages plus one rate-limit warning. Always size your alerting off counters or flow data, never off a message count.
  • Log on permits, not just denies. Most people only put log on the implicit deny, which means they only see traffic their policy already stopped. The interesting traffic is the traffic you are allowing.
  • Leave an unlogged catch-all at the bottom. Without a final permit ip any any carrying no log, every packet on the interface generates logging and the router spends its day building syslog messages instead of forwarding.
  • ACL logging is process-switched. That is precisely why IOS rate-limits it. Do not respond to the rate-limit message by raising the cap on a production edge router without understanding what you are asking the CPU to do.
  • Read counters twice. Separate reads of the same ACL during and after the scan gave different totals on the SSH line, because background management traffic keeps ticking. Diff two timestamped reads rather than trusting one snapshot.
  • A quiet SSH log does not mean nobody knocked. The SYN scan found port 22 open and never completed a handshake, so sshd has nothing to report. Application logs are blind to half-open scanning by design.

Key Takeaways

  • A permit ... log ACL turns any IOS XE device into a scan sensor with no extra licensing. The signature is a lopsided ACE hit count: 1420 matches on a generic line against single digits on the real service lines.
  • The behavioural tell in syslog is one source address reaching many different destination ports in a short window, one packet per flow. Real traffic inverts that ratio.
  • %SEC-6-IPACCESSLOGRL is an alert in its own right. It reported 1423 missed packets here, which means the burst was real and that syslog alone will always undercount it.
  • In a capture, a SYN scan is unmistakable: identical sequence numbers from a single source port, bare SYNs with a 1024-byte window, RSTs coming back from closed ports, and an [R] from the attacker on the one port that answered.
  • Slow scans defeat every rate-based signal but not the cumulative counter. Diffing show ip access-lists over time is one of the few cheap ways to see enumeration spread across days.
  • Source-based detection is defeated by decoys and spoofing, which is why this belongs alongside the rest of your Cisco device hardening controls rather than standing in for them.

Read next