Skip to main content
友田 陽大
実践ネットワーク攻撃と防御
セキュリティ
ネットワーク
TCP/IP
脆弱性診断
ホワイトハッカー
可観測性

How port scanning / service reconnaissance (nmap) works and its defense [2026] — visualizing the attack surface and RFC-compliant reduction

An explanation of 'port scanning,' the core of network reconnaissance, faithful to the nmap official documentation and NIST SP 800-115. It explains, from TCP's state transitions, how host discovery, TCP SYN scan, and version/OS detection work, and why a stealth scan is detected. With all-legal procedures confined to your own lab, it shows the defenses — attack-surface minimization, IDS detection, and security-group design — in type-safe code.

Published
Reading time
9 min read
Author
友田 陽大
Share

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 nmap commands 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"

StateMeaningThe defender's reading
openA service is listeningAttack surface. Ask whether exposure is truly needed
closedResponds but no serviceThe host's existence is revealed. Room to hide with a FW
filteredBlocked by FW/SGThe 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.

nmap output 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. nmap as 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 open to filtered.
  • 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 nmap is 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.

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

Could this attack succeed on your network?

Network / infrastructure penetration testing & hardening

The L2–L4 attacks covered here — ARP spoofing, DNS poisoning, session hijacking, SYN flood, sniffing — I diagnose where your configuration breaks from an attacker's perspective, and design and implement RFC-compliant defenses (DAI, DNSSEC, RFC 5961, BCP 38, TLS/mTLS, WAF/DDoS protection). With experience building multi-layered networks on AWS multi-account (VPC, least-privilege IAM, GuardDuty, WAF), I help minimize the attack surface and move toward zero trust.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading