こんにちは。現場の泥をすすりながら、数々のネットワーク障害と戦い続けてきたシニアネットワークエンジニアだ。
最近、エンタープライズの現場では「境界型防御の限界」が叫ばれ、猫も杓子も「ゼロトラストネットワークアクセス(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パケットそのものなのだから。
現場からは以上だ。健闘を祈る。
コメント