【実務・中級編】 ZTNA導入における接続断・遅延発生時のパケットキャプチャ解析手法 – ゼロトラスト&エンタープライズセキュリティ実践ガイド

こんにちは。現場の泥をすすりながら、数々のネットワーク障害と戦い続けてきたシニアネットワークエンジニアだ。

最近、エンタープライズの現場では「境界型防御の限界」が叫ばれ、猫も杓子も「ゼロトラストネットワークアクセス(ZTNA)」への移行を進めている。オンプレミスのレガシーなVPN装置を廃止し、クラウドベースのZTNAゲートウェイ(PEP: Policy Enforcement Point)を導入するプロジェクトが花盛りだ。

しかし、いざ導入してみると、運用現場にはこんな悲鳴が響き渡る。
「特定のWeb APIの呼び出しだけ、なぜか3秒に1回タイムアウトする」
「ZTNAクライアントを有効にした途端、一部のセッションが不定期に切断される」

VPN時代なら、ゲートウェイのインターフェースで tcpdump を回せば、生IPパケットのルーティングやNATの不整合が一目瞭然だった。だが、ZTNAは違う。通信のほとんどは、TLS 1.3やUDP(DTLS/QUIC)をベースにした高度な暗号化トンネルに包まれている。パケットキャプチャを開いても、目に飛び込んでくるのは無機質な暗号データの羅列(Application Data)だけだ。

「暗号化されているから解析できません」と諦めてベンダーに丸投げするか? それではプロとは言えない。
今回は、ZTNA環境における接続断・遅延トラブルを解決するために、暗号化の壁を突破してパケットを解析する、泥臭くも極めて実践的なアプローチを伝授しよう。パケットは絶対に嘘をつかない。その真実を暴き出す方法を、順を追って解説する。

—

1. ZTNAにおける通信アーキテクチャとシーケンス

まずは、我々が対峙している敵(ZTNAの通信フロー)の全体像を整理しよう。
多くのZTNA製品(Zscaler、Cloudflare One、Prisma Accessなど)は、クライアント端末に常駐するエージェント(ZTNAクライアント)と、クラウド上のZTNAゲートウェイ間で暗号化トンネルを確立する。

RFC 8446で規定されるTLS 1.3や、RFC 9000のQUIC(UDPベース)がその主役だ。

+------------------+          +-------------------+          +-------------------+
|  ZTNA Client     |          |  ZTNA Gateway     |          |  Internal API /   |
|  (User Device)   |          |  (PEP / Proxy)    |          |  Web Server       |
+--------+---------+          +---------+---------+          +---------+---------+
         |                              |                              |
         |----- 1. TCP Handshake ------>|                              |
         |<---- TCP SYN-ACK ------------|                              |
         |                              |                              |
         |----- 2. TLS Client Hello --->|                              |
         |<---- TLS Server Hello -------|                              |
         |      (Key Exchange)          |                              |
         |                              |                              |
         |<==== 3. Encrypted Tunnel ===>|                              |
         |      (Mutual Auth & Policy)  |                              |
         |                              |                              |
         |                              |----- 4. TCP Handshake ------>|
         |                              |<---- TCP SYN-ACK ------------|
         |                              |                              |
         |                              |----- 5. Forwarded Request -->|
         |                              |<---- 6. API Response --------|
         |                              |                              |
         |<==== 7. Encrypted Tunnel ===>|                              |
         |      (Return Data)           |                              |

このシーケンスにおいて、トラブルのボトルネックになりやすいのは以下の3箇所だ。

1. Client ⇔ Gateway 間のネットワーク品質(パケットロス・遅延)
2. Client ⇔ Gateway 間のTLSネゴシエーション、または認可ポリシー評価のオーバーヘッド
3. Gateway ⇔ バックエンド(APIサーバー等)間のルーティング・名前解決遅延

これらを切り分けるために、まずはパケットの「中身」を覗く準備をする必要がある。

—

2. 暗号化の壁を突破する:SSLKEYLOGFILE の活用

パケットキャプチャでTLSの中身をデバッグする際、絶対に欠かせないのが「セッション鍵(Pre-Master Secret)」の抽出だ。本番環境のサーバーの秘密鍵をWiresharkにインポートする手法は、TLS 1.3では前方秘匿性(PFS: Perfect Forward Secrecy)の導入により通用しない。

現代のエンジニアが取るべきアプローチは、クライアントプロセスに一時的なキーログファイルを吐き出させ、それをWiresharkに食わせる方法だ。

Windows環境での設定方法

デバッグ対象の端末で、環境変数 SSLKEYLOGFILE を設定してブラウザやZTNAクライアント(一部のGo製やRust製クライアント、Electronベースのもの)を起動する。

1. システム環境変数、またはユーザー環境変数に以下を追加する。

  • 変数名: SSLKEYLOGFILE
  • 変数値: C:\temp\sslkey.log(任意の書き込み可能なパス)

2. 変更を反映するため、コマンドプロンプトやPowerShellを「管理者として実行」し、そこから対象のブラウザ(ChromeやEdge)を起動する。

# PowerShellでの一時的な環境変数設定と起動例
$env:SSLKEYLOGFILE="C:\temp\sslkey.log"
Start-Process "C:\Program Files\Google\Chrome\Application\chrome.exe" --new-window "https://your-ztna-app.internal"

Linux / macOSでのデバッグ起動

開発用のLinuxクライアントやMacから、テスト用の curl や検証スクリプトを走らせる場合は、コマンドの前に環境変数を付与するだけでよい。

# キーログファイルを指定してcurlを実行
export SSLKEYLOGFILE=~/sslkey.log
curl -iv https://your-ztna-app.internal/api/v1/status

Wiresharkへのキーロード設定

1. Wiresharkを起動し、メニューから [編集(Edit)] -> [設定(Preferences)] を開く。
2. 左側のリストから [Protocols] -> [TLS](古いバージョンでは [SSL])を選択する。
3. [(Pre)-Master-Secret log filename] の項目で、先ほど出力先に指定した sslkey.log を選択する。

これで準備は完了だ。暗号化されていた Application Data が、まるで魔法のようにHTTP/1.1やHTTP/2、あるいはgRPCの生データへとデコードされ、不具合の核心に迫ることができる。

—

3. 遅延発生時のボトルネック特定手法

ZTNAを介した通信で「遅延(レイテンシ)」が発生しているとき、それが「ネットワークの遅延」なのか、「ZTNAゲートウェイのポリシー評価・認証遅延」なのか、はたまた「バックエンドサーバーの処理遅延」なのかを明確に区別しなければならない。

これを見極めるために、curl のタイムスタンプ書き出し機能(-w オプション)を活用したデバッグ用設定ファイルを作成しよう。

実践:curl を用いたネットワーク・TLS・アプリケーション遅延の分解測定

以下の設定ファイルを curl-format.txt として保存する。

# curl-format.txt
\n
            DNS Lookup Time:  %{time_namelookup}s\n
               TCP Connect:  %{time_connect}s\n
            TLS Handshake:  %{time_appconnect}s\n
             Pre-Transfer:  %{time_pretransfer}s\n
               Redirected:  %{time_redirect}s\n
          Start Transfer (TTFB):  %{time_starttransfer}s\n
                         -------------------------\n
               Total Time:  %{time_total}s\n
\n

このフォーマットファイルを使って、ZTNA経由でAPIにリクエストを投げる。

# ZTNA経由のAPIサーバーに対して、詳細な時間計測を実施
curl -w "@curl-format.txt" -o /dev/null -s -k https://your-ztna-app.internal/api/v1/data

出力結果の読み解き方

DNS Lookup Time:  0.004123s
               TCP Connect:  0.045210s  <-- (A) 端末からZTNAゲートウェイまでの往復遅延 (RTT)
            TLS Handshake:  0.185432s  <-- (B) 暗号化セッション確立までの累積時間
          Start Transfer (TTFB):  0.895122s  <-- (C) ゲートウェイがバックエンドを叩き、最初の1バイトを返すまでの時間
                         -------------------------
               Total Time:  0.901245s
  • (A) TCP Connect が極端に大きい場合(例: > 200ms)

端末からZTNAのPOP(Point of Presence)までの物理的なネットワーク、またはISP経路に問題がある。最寄りのエッジサーバーにルーティングされていない可能性がある(DNSによるジオロケーションの失敗など)。

  • (B) TLS Handshake までの時間が異常に長い場合

ZTNAゲートウェイにおけるクライアント証明書の検証(CRL/OCSP確認)や、アイデンティティプロバイダ(IdP)とのSAML/OIDC連携による認可チェックがボトルネックになっている。

  • (C) TTFB (Time to First Byte) が突出して遅い場合

ZTNAゲートウェイとバックエンドサーバー間の通信遅延、もしくはバックエンドのDBクエリ処理自体が低速化している。ZTNAそのもののせいではない可能性が高い。

—

4. Wiresharkで「不意の接続断」を解析する

接続が突然切れる「セッション瞬断」のトラブルシューティングでは、Wiresharkのフィルタリング機能を用いて、パケットレベルの挙動を追跡する。

フィルター1:TCPリセット(RST)の送信元を特定する

セッションが強制終了されるとき、パケットには RST フラグが立つ。誰がリセットを投げたのかを特定することは極めて重要だ。

tcp.flags.reset == 1
  • IPヘッダのTTL(Time to Live)に注目せよ

通常、パケットがルーターを通過するたびにTTLは減少する。
クライアントが受け取った通常のパケットのTTLが 64(Linux標準などから減算されて例えば 56)であるのに対し、RSTパケットのTTLだけが突如 255 や 128 で届いた場合、それは通信経路上のファイアウォールやZTNAゲートウェイが、「送信元を偽装して割り込み(TCP Reset Injection)をかけた」動かぬ証拠となる。

フィルター2:TLS Alertによる暗号化ネゴシエーションの失敗

TLSのハンドシェイク中に切断される場合、コントロールプレーンのログには単に「Connection Closed」としか出ないことが多い。Wiresharkで以下のフィルターを実行し、TLS層が吐き出したエラーコードを直接確認する。

tls.record.content_type == 21

パケット詳細ペインの Transport Layer Security -> TLS Record Layer -> Alert を確認する。

  • Alert (Level: Fatal, Description: Bad Certificate)

クライアント証明書が期限切れ、もしくはZTNAゲートウェイが信頼していない認証局から発行されている。

  • Alert (Level: Fatal, Description: Handshake Failure)

クライアントとゲートウェイ間で、サポートする暗号スイート(Cipher Suite)が一致していない。ZTNA側が TLS 1.3 のみを強制しているにもかかわらず、古いクライアントが TLS 1.2(かつ古い暗号)で挑んでいる可能性がある。

—

5. 自動デバッグ:Pythonスクリプトによるパケット損失とパケット往復時間の監視

トラブルが「たまにしか発生しない」場合、エンジニアがPCの前に張り付いてキャプチャし続けるわけにはいかない。
以下は、Pythonの scapy ライブラリを使用して、ZTNAゲートウェイとの間のTCP/TLS接続時における、パケット再転送(Retransmission)とRTT(Round Trip Time)をバックグラウンドで継続的に監視・記録するスクリプトの例だ。

実務でそのまま検証に使えるよう、例外処理や丁寧なコメントを記述してある。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
ZTNA-Gateway Connection Quality Monitor
このスクリプトは、指定したZTNAゲートウェイのIP/ポート宛てのTCPパケットを監視し、
再転送(Retransmission)や異常な遅延を検知した際にログを出力します。
※実行には管理者権限(sudo)が必要です。
"""

import sys
from scapy.all import sniff, TCP, IP

# 監視対象の設定(検証環境のZTNAゲートウェイIP、ポートを指定)
TARGET_GATEWAY_IP = "192.0.2.100"  # ダミーのZTNAゲートウェイIP
TARGET_PORT = 443                  # 通常はHTTPS/TLSの443

# パケットシーケンス追跡用の辞書
sequence_tracker = {}

def packet_callback(packet):
    # TCP層とIP層が存在することを確認
    if packet.haslayer(IP) and packet.haslayer(TCP):
        ip_src = packet[IP].src
        ip_dst = packet[IP].dst
        tcp_sport = packet[TCP].sport
        tcp_dport = packet[TCP].dport
        seq_num = packet[TCP].seq
        ack_num = packet[TCP].ack
        
        # クライアントからZTNAゲートウェイへの送信パケットを追跡
        if ip_dst == TARGET_GATEWAY_IP and tcp_dport == TARGET_PORT:
            session_key = (ip_src, tcp_sport, ip_dst, tcp_dport)
            
            if session_key in sequence_tracker:
                # 既に記録されているシーケンス番号と同じパケットが再度送られた場合
                # これはパケットロスによる「再転送(Retransmission)」を意味する
                if seq_num in sequence_tracker[session_key]:
                    print(f"[!] パケット再転送を検知! [Client Port: {tcp_sport}] -> [Seq: {seq_num}]")
                    print(f"    --> 経路上のパケットロス、またはZTNAゲートウェイの過負荷の可能性あり。")
                else:
                    sequence_tracker[session_key].add(seq_num)
            else:
                sequence_tracker[session_key] = {seq_num}

def main():
    print(f"[*] ZTNA監視スクリプトを起動しました。")
    print(f"[*] 対象ゲートウェイ: {TARGET_GATEWAY_IP}:{TARGET_PORT}")
    print(f"[*] パケットのキャプチャを開始します... (中断するには Ctrl+C)")
    
    # BPFフィルターを設定して、不要なトラフィックを削ぎ落とし負荷を軽減
    bpf_filter = f"tcp and host {TARGET_GATEWAY_IP} and port {TARGET_PORT}"
    
    try:
        sniff(filter=bpf_filter, prn=packet_callback, store=0)
    except KeyboardInterrupt:
        print("\n[*] 監視を正常に終了しました。")
        sys.exit(0)
    except PermissionError:
        print("[-] エラー: スクリプトの実行には管理者権限(sudoなど)が必要です。")
        sys.exit(1)

if __name__ == "__main__":
    main()

—

6. まとめ:境界型から脱却したエンジニアが持つべき「マインドセット」

従来の「境界防御(オンプレミスVPN)」の時代は、ネットワークを物理的、あるいはVLAN論理的な「土管」として捉え、その土管の入り口と出口を監視していれば十分だった。

しかし、ゼロトラスト(ZTNA)の世界は異なる。ネットワークは動的であり、通信は常にポリシーによって暗号化のベールに包まれ、アイデンティティ(ID)やコンテキスト(デバイスのパッチ適用状況など)と紐付いて制御されている。

ここで発生するトラブルは、単なる「物理レイヤーの不調」ではなく、「ネットワーク、暗号、アイデンティティ認可の複合的な不整合」であることがほとんどだ。

だからこそ、トラブルシューティングに臨む際には、以下のステップを徹底してほしい。

1. まず暗号を剥がす準備をする(SSLKEYLOGFILE の確保、テスト用クライアントでの事前検証環境の構築)。
2. 時間を細分化する(DNS、TCP、TLS、TTFBのどこでミリ秒が消費されているかを curl 等で数値化する)。
3. パケットの「振る舞い」を見る(TCP RSTのTTL、TLS Alertのメッセージコードから、誰が通信を遮断したのか、その「犯人の意思」をパケットから読み取る)。

「ブラックボックス化されたゼロトラスト製品」に振り回されるな。どんなに高度なアーキテクチャであっても、最終的にネットワークを流れるのは、かつて我々が学び、愛したTCP/IPやUDPパケットそのものなのだから。

現場からは以上だ。健闘を祈る。

コメント

タイトルとURLをコピーしました