Network attacks begin, almost without exception, with a port scan. The first thing an attacker does is learn "which hosts are alive, which ports are open, and what's running there" — the core of NIST SP 800-115's Discovery phase. Conversely, if the defender doesn't accurately grasp their own attack surface with nmap, they're in a dangerous state where the attacker knows their network better than they do.
Following the big picture of network pentesting, this article explains how port scanning works faithfully to the nmap official documentation, and on top of that shows the defenses of attack-surface minimization and detection in type-safe code.
Strict safe zone: all the
nmapcommands in this article are run only against a VM you manage yourself (e.g.,10.10.10.10) in your home lab. Scanning an unauthorized third-party host or public server, beyond being an issue under the Unauthorized Access Act, is a harmful act that can be recorded by the other party's IDS and reported. You may scan only "your own assets, CTFs, or a scope authorized in writing" — always read the law article first.
1. The principle of port scanning — "quitting the TCP handshake midway"
Port scanning exploits the behavior of the TCP three-way handshake. When you send SYN to a port, the other side's reaction reveals the state.
■ ポートが「開いている(open)」場合
scanner ── SYN ──► target:port "開いてる?"
scanner ◄─ SYN/ACK ─ target "うん、開いてるよ" ← これで「open」と判定
scanner ── RST ──► target "やっぱやめた"(接続を張らずに中断)
■ ポートが「閉じている(closed)」場合
scanner ── SYN ──► target:port
scanner ◄─ RST ──── target "そのポートは閉じてる" ← 「closed」と判定
■ ファイアウォールに「遮断されている(filtered)」場合
scanner ── SYN ──► target:port
(無応答 / ICMP unreachable) ← 「filtered」と判定(FWの存在が分かる)
This — "send SYN, judge open if SYN/ACK returns, but don't return ACK and abort with RST" — is the TCP SYN scan (-sS, half-open scan). Because it doesn't establish a full connection (ESTABLISHED), it was once called "stealth."
1.1 "Stealth scan" is transparent nowadays
Let me correct an important misconception. A SYN scan is easily detected by modern IDS/firewalls. The reason is simple: the very behavior of "sending only SYN to many ports in a short time, and not returning ACK to SYN/ACK but aborting with RST" is a clear signature impossible in normal communication. Not letting the attacking side hold the illusion of "stealth" is the defender's starting point (we implement this detection in §4).
1.2 The three states become "a map for defense"
| State | Meaning | The defender's reading |
|---|---|---|
| open | A service is listening | Attack surface. Ask whether exposure is truly needed |
| closed | Responds but no service | The host's existence is revealed. Room to hide with a FW |
| filtered | Blocked by FW/SG | The ideal state. Gives the attacker no information |
The goal of defense is clear — move ports that shouldn't be exposed from open to filtered.
2. The reconnaissance procedure — see "your own attack surface" in the lab
Against your own VM, visualize the attack surface from the same viewpoint as an attacker.
# ① ホスト発見:ラボのセグメントで「生きているホスト」を洗い出す(自分のVMのみ)
nmap -sn 10.10.10.0/24
# -sn = ポートスキャンせずホスト発見だけ(ping sweep)
# ② TCP SYN スキャン:開いているポートを特定(自分のVM 10.10.10.10 のみ)
sudo nmap -sS 10.10.10.10
# sudo が要るのは raw socket で SYN を直接組み立てるため
# ③ サービス・バージョン検出:開いたポートで「何が動いているか」を特定
nmap -sV 10.10.10.10
# 例: 22/tcp open ssh OpenSSH 8.9 / 80/tcp open http nginx 1.24.0
# ↑ 版数が分かると、その版の既知脆弱性(CVE)に直結する=最も危険な情報漏れ
# ④ OS 推定 + デフォルトスクリプト(軽い既知チェック)
sudo nmap -O -sC 10.10.10.10
# -O = TCP/IP スタックの癖から OS を推定
# -sC = 安全寄りのデフォルトNSEスクリプト(バナー取得等)
The biggest lesson for the defender is ③. When -sV exposes even the version like nginx 1.24.0, an attacker can immediately look up "the known CVEs of that version." You can feel how important it is to hide / keep up to date the version in the service banner.
nmapoutput can be saved as XML with-oX scan.xml. It can be used for a diagnostic report or for the "continuous monitoring of your own attack surface" described later.nmapas an inventory of your own assets is a legitimate, essential skill for the defender.
3. Defense ①: attack-surface minimization — crush it mechanically with configuration
The strongest defense against scanning is to reduce the open ports in the first place. This is achievable mechanically with design and configuration.
3.1 The principle — "minimum necessary exposure"
- Don't expose management ports (SSH:22, RDP:3389, DB:5432/3306). Use a bastion, or on AWS, SSM Session Manager (connect without opening any port).
- Put databases and internal APIs in a private subnet. Make them unreachable from the internet.
- Consolidate exposure to 443 (HTTPS) and branch the route behind it.
3.2 Write an AWS Security Group with "minimal permission"
In the cloud, the design of the firewall = security group (SG) is itself the attack surface. Opening SSH with 0.0.0.0/0 is a typical accident.
# ❌ 事故の典型:SSH を全世界に開放(スキャンで即発見され総当たりの的に)
resource "aws_security_group_rule" "bad_ssh" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # ← 攻撃面を全世界に晒している
security_group_id = aws_security_group.app.id
}
# ✅ 正:公開は 443 のみ。管理は SSM 経由でポートを開けない
resource "aws_security_group" "app" {
name = "app-minimal-surface"
description = "Public 443 only; admin via SSM (no inbound SSH)"
vpc_id = var.vpc_id
ingress {
description = "HTTPS from anywhere (terminate at ALB/WAF)"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# SSH(22) の ingress は「無い」のが正解。SSM がポートレスで代替する。
egress {
description = "Allow outbound (tighten per workload)"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "app-minimal-surface" }
}
The point: attack-surface minimization isn't "be careful" but expressing it in code (IaC) and guarding it with review and diffs. This is the core thinking from my designing and operating multi-layer networks on AWS.
4. Defense ②: detect the scan — type-safe detection logic
Even if prevention (attack-surface reduction) is slipped past, a scan has a clear behavioral signature: "in a short time, one source attempts connections to many different ports/hosts." Detect this.
4.1 In production, an IDS (Suricata) is the standard
In production, an IDS/IPS like Suricata or Snort detects port scans with off-the-shelf rules. On AWS, VPC flow logs + GuardDuty raise Recon:* findings. Riding on managed/off-the-shelf detection first is the correct answer.
4.2 A minimal implementation to understand the mechanism (type-safe)
To understand the principle of detection, write a pure function that judges "scan-likeness" from flow logs (or connection events). This isn't production logic but visualization of the principle.
/** 1件の接続試行(フローログの1行を正規化したもの)。 */
interface ConnectionAttempt {
readonly srcIp: string;
readonly dstPort: number;
readonly timestamp: number; // epoch ms
}
interface ScanVerdict {
readonly srcIp: string;
readonly distinctPorts: number;
readonly isLikelyScan: boolean;
}
/**
* 「短時間に同一送信元が多数の異なるポートへ接続を試みた」かを判定する純粋関数。
* 副作用なし=単体テスト・ゴールデンベクタ固定が容易(テスト容易性)。
*
* @param windowMs 観測窓(既定60秒)
* @param portThreshold この窓で触れた異なるポート数の閾値(既定15)
*/
export function detectPortScan(
attempts: readonly ConnectionAttempt[],
now: number,
windowMs = 60_000,
portThreshold = 15,
): readonly ScanVerdict[] {
const since = now - windowMs;
const portsBySrc = new Map<string, Set<number>>();
for (const a of attempts) {
if (a.timestamp < since) continue; // 窓の外は無視
const ports = portsBySrc.get(a.srcIp) ?? new Set<number>();
ports.add(a.dstPort);
portsBySrc.set(a.srcIp, ports);
}
return [...portsBySrc.entries()]
.map(([srcIp, ports]) => ({
srcIp,
distinctPorts: ports.size,
isLikelyScan: ports.size >= portThreshold,
}))
.filter((v) => v.isLikelyScan)
.sort((a, b) => b.distinctPorts - a.distinctPorts); // 重い順に並べる(決定的)
}
With this pure function, you can call it from a flow-log batch or a CloudWatch Logs subscription and connect it to a Slack notification or automatic blocking (within a range that doesn't break the allowlist). By decoupling the detection logic from side effects, you can fix the behavior in tests and safely exclude false positives (your own legitimate scans, health checks) with an allowlist.
Prevention and detection are different, and both are needed: attack-surface minimization (§3) is the prevention of "don't show it in the first place," and scan detection (§4) is the discovery of "notice when you're seen." Either one alone is insufficient. This is the basic stance of defense in depth.
5. Conclusion
- Port scanning is the starting point of all attacks. The SYN scan (-sS) quits the handshake midway and judges open/closed/filtered. "Stealth" is an illusion, transparent to modern IDS.
- The most dangerous information leak is the service version (
-sV). It connects directly to known CVEs. Hide the version and keep it up to date. - Defense ① = attack-surface minimization: don't expose management ports, IaC the SG with minimal permission, and substitute with SSM/bastion. Move
opentofiltered. - Defense ② = detection: capture the scan's behavioral signature (short time, many ports, single source) with an IDS/flow logs. Separate the detection logic into a pure function to make it testable.
- Taking inventory of your own company with your own
nmapis a legitimate, essential skill for the defender. Be more knowledgeable about your company than the attacker.
Next, we cover ARP spoofing / man-in-the-middle (MITM), which intrudes on the route itself at L2.
I (Yudai Tomoda) have IaC'd AWS security groups, NACLs, and VPC design with least privilege, and implemented attack-surface management including reconnaissance detection with GuardDuty and flow logs. "I want to take inventory of our public ports," "I'm anxious that the SGs are full of 0.0.0.0/0," "I want to detect scans/reconnaissance" — I diagnose this visualization and reduction of the attack surface from an attacker's viewpoint and cure it at the root with both configuration (IaC) and detection. Feel free to consult me.