【実務・中級編】 ZTNA環境におけるレガシーアプリケーション(非Web/非HTTP)の収容とプロキシ技術 – ゼロトラスト&エンタープライズセキュリティ実践ガイド

レガシーな社内システムをどうやって「ゼロトラスト」の網に絡め取るか?――SSH・RDP・独自TCPをZTNAで安全に収容する実務アプローチ

こんにちは、ネットワークセキュリティの現場を渡り歩いてきたシニアエンジニアの私です。

「社内のゼロトラスト化を進めるぞ!」と威勢よく号令がかかったものの、プロジェクトの終盤で必ずと言っていいほど、私たちの前に立ち塞がる巨大な壁があります。そう、「レガシーアプリケーション」です。

今時のイケてるシステムなら、OAuth 2.0やOIDCによる認証がスマートに組み込まれ、HTTPSのAPI通信で綺麗に完結しています。しかし、現場の泥臭いシステムを見てください。
暗号化されていない(あるいは古い暗号スイートの)SSH、いつの間にか野良で踏み台サーバに直結されているRDP、そして仕様書すらないメーカー製専用クライアントが喋る謎のTCPベースの独自プロトコル……。これらは「Webブラウザでポチポチ認証して終わり」というモダンなZTNA(ゼロトラストネットワークアクセス)の王道アプローチだけでは、一筋縄ではいきません。

今回は、そんな非Web・非HTTPのレガシーアプリケーションを、いかにしてZTNAの強固な統制下に安全に収容するか、そのアーキテクチャと具体的なプロキシ技術の実装手法を、現場のリアルな知見を交えて徹底解説します。

—

1. 境界型防御の呪縛と、レガシー通信収容のジレンマ

かつてのエンタープライズネットワークは、「社内LANという安全な城壁」の中にさえ入れば、あとはフリーパスで全てのサーバーと通信できる世界でした。一度VPNゲートウェイを突破してしまえば、社内のあらゆるRDPポート(TCP/3389)やSSHポート(TCP/22)にアクセスし放題。これが従来の境界型防御の姿です。

ゼロトラストの基本思想は「Never Trust, Always Verify(決して信頼せず、常に検証する)」です。
これはWebアプリに限った話ではありません。たとえそれがレガシーなSSH接続であっても、「誰が、どの端末から、どの権限で、どの宛先へアクセスしているか」をセッション単位で検証し、最小権限の原則を強制しなければなりません。

しかし、ここでエンジニアなら誰もが直面するジレンマがあります。
「HTTPプロキシやリバースプロキシの仕組みはWebアプリには効くが、パケットの生バイナリを流す独自TCPプロトコルに、どうやって認証やコンテキスト検証を割り込ませるのか?」という問題です。

答えは「トンネリングとプロキシの二段構え」にあります。クライアント側の専用エージェント(または軽量プロキシ)で通信をカプセル化し、ZTNAのポリシーエンジン(PEP/PDP)で認可を通した上で、安全なトンネル経由でレガシーな宛先へ流し込むのです。

—

2. 通信フロー:ZTNA環境におけるTCPトンネリングの裏側

まずは、非Webプロトコル(例としてRDPやSSH)をZTNAゲートウェイ経由で安全に利用する際の、パケットと制御のシーケンスを見てみましょう。単にポートを開けるのではなく、コネクション確立の裏側で厳密なアイデンティティ検証が行われます。

[クライアント端末]                  [ZTNA ゲートウェイ]             [レガシーアプリサーバー]
  (RDP/SSHクライアント)               (PEP / プロキシ)                 (社内ネットワーク)
        │                                   │                               │
        │── 1. 接続要求 ───────────────────>│                               │
        │   (デバイス証明書 + ユーザー認証)  │                               │
        │                                   │── 2. PDPへポリシー問い合わせ ──>│ (認可判定)
        │<── 3. 認証・認可OK (トンネル確立)─│                               │
        │                                   │                               │
        │── 4. カプセル化されたTCPパケット ─>│                               │
        │    (TLS等で保護されたトンネル)    │── 5. 生のTCPパケットに変換 ──>│
        │                                   │    (TCP/3389 or TCP/22)       │
        │                                   │<── 6. レガシー応答 ───────────│
        │<── 7. トンネル経由で応答返却 ─────│                               │

このフローの肝は、「クライアントからゲートウェイまでの区間はゼロトラストで保護されたトンネル(多くはHTTPSベースのWebSocketやgRPC、あるいは相互TLS)」であり、「ゲートウェイからサーバーまでの区間は、必要に応じて最小限のファイアウォール制御やローカルプロキシで保護された安全な閉域網」という点です。これにより、インターネット上にレガシーポートを直接露出させずに済みます。

—

3. 実践:SSH/RDPをZTNAの網に収容する設定とプロキシ技術

ここからは、実務でよく使われる具体的な設定やコード例を見ていきましょう。
今回は、オープンソースやエンタープライズ製品でもよく見られる、「軽量プロキシによるTCPフォワーディング(L4プロキシ)」のアプローチを解説します。

パターンA: SSHをZTNAのアイデンティティ認識型プロキシ経由で接続する

多くのZTNAソリューション(Cloudflare Access、Tailscale、あるいはオープンソースのTeleportなど)は、SSHクライアントのProxyCommandや設定ファイルを書き換えることで、既存のワークフローを崩さずに統合できます。

以下は、SSHのクライアント設定ファイル(~/.ssh/config)の記述例です。

# ~/.ssh/config の設定例
# レガシーな社内踏み台サーバーへの接続をZTNAプロキシ経由にルーティングする

Host legacy-jump-server
    # 実際の宛先IPではなく、ZTNAゲートウェイのプロキシエンドポイントを指定
    HostName ztna-proxy.internal.example.com
    Port 443
    
    # ユーザー名
    User admin_user
    
    # 標準のSSH接続ではなく、HTTPSベースのWebSocketトンネル(例: 踏み台コマンド)を経由させる
    # ここでは例として、カスタムのラッパースクリプトやZTNAクライアントのCLIを指定
    ProxyCommand /usr/local/bin/ztna-cli ssh-proxy --target %h:%p --identity-file ~/.ssh/id_ed25519
    
    # 接続維持とセキュリティのためのパラメータ
    ServerAliveInterval 60
    ServerAliveCountMax 3
    IdentitiesOnly yes

> シニアからの現場Tips:
> ProxyCommandを使う手法は、開発者の手元のマシン環境(macOS/Linux)に非常に馴染みます。しかし、Windows環境の古いRDPクライアント(mstsc.exe)などではこれが使えません。その場合は、ローカルループバック(例: 127.0.0.1:3389)にバインドする常駐型の軽量ローカルプロキシデーモンをクライアント端末で起動させる方式をとるのが定石です。

—

パターンB: PythonによるカスタムTCPトンネリング・プロキシの概念実装

「市販のZTNA製品ではなく、自社でプライベートなセキュアアクセス基盤を構築したい、あるいはその挙動を深く理解したい」というエンジニア向けに、Pythonを使った簡易的なTCPリバース/フォワードプロキシの骨組みを紹介します。

実際のZTNAではここにmTLS(相互TLS)やJWTによる認可トークンの検証が挟まりますが、ソケット通信の基本構造は同じです。

import socket
import threading
import sys

def handle_client(client_socket, target_host, target_port):
    """
    クライアントからのTCPストリームを受け取り、
    ZTNAゲートウェイ(またはバックエンドのレガシーサーバー)へ転送するハンドラー
    """
    try:
        # バックエンドのレガシーサーバー(例: 内部RDPやSSH)への接続を確立
        remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        remote_socket.connect((target_host, target_port))
    except Exception as e:
        print(f"[-] バックエンドサーバーへの接続に失敗しました ({target_host}:{target_port}): {e}", file=sys.stderr)
        client_socket.close()
        return

    # 双方向のデータ転送を行うためのスレッド関数
    def forward(source, destination):
        try:
            while True:
                data = source.recv(4096)
                if not data:
                    break
                destination.sendall(data)
        except Exception:
            pass
        finally:
            source.close()
            destination.close()

    # クライアント -> サーバー、サーバー -> クライアント の双方向通信を非同期で開始
    t1 = threading.Thread(target=forward, args=(client_socket, remote_socket))
    t2 = threading.Thread(target=forward, args=(remote_socket, client_socket))
    
    t1.start()
    t2.start()

def start_ztna_tcp_proxy(local_port, target_host, target_port):
    """
    ZTNAプロキシのリスナーを起動する
    """
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # サーバーの再利用を許可
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(('127.0.0.1', local_port))
    server.listen(5)
    
    print(f"[*] ZTNA TCPプロキシが起動しました: 127.0.0.1:{local_port} -> {target_host}:{target_port}")
    
    try:
        while True:
            client_socket, addr = server.accept()
            print(f"[+] 接続を検知しました: {addr[0]}:{addr[1]}")
            
            # 本番環境では、ここでZTNAのセッション検証やアクセス制御ポリシーの評価を行う
            
            client_handler = threading.Thread(
                target=handle_client,
                args=(client_socket, target_host, target_port)
            )
            client_handler.start()
    except KeyboardInterrupt:
        print("\n[*] プロキシを停止します。")
    finally:
        server.close()

if __name__ == '__main__':
    # 例: ローカルのポート3389番へのアクセスを、内部のレガシーRDPサーバーへトンネリングする
    LOCAL_PORT = 3389
    LEGACY_TARGET_HOST = "10.100.50.20"  # 社内閉域網内のレガシーサーバー
    LEGACY_TARGET_PORT = 3389  # RDPの標準ポート
    
    start_ztna_tcp_proxy(LOCAL_PORT, LEGACY_TARGET_HOST, LEGACY_TARGET_PORT)

このスクリプトはあくまで概念実証(PoC)ですが、「ローカルのポートで受け、バリデーションを通した上で、安全に宛先へバイナリを流し込む」というZTNAプロキシの本質を非常にシンプルに表しています。

—

4. 現場でハマる!デバッグと運用時の注意点

最後に、レガシーアプリをZTNAに収容するプロジェクトで、エンジニアたちが血を流しやすい「ハマりどころ」とトラブルシューティングの勘所をいくつか共有しておきます。

1. タイムアウトとキープアライブ(Keep-Alive)の調整

レガシーなデータベース接続や太いSSHセッション、長時間のRDPセッションでは、途中で通信がプッツリ切断されるトラブルが頻発します。

  • 原因: ZTNAゲートウェイやL4ロードバランサーの「アイドルタイムアウト(例: 60秒〜300秒)」に引っかかっているケースがほとんどです。
  • 対策: TCPのキープアライブパケット(TCP Keepalive)のインターバルを短く設定するか、プロキシ層で定期的なヘルスチェックパケットを流すようにチューニングしてください。先ほどのSSH設定例にあった ServerAliveInterval 60 もその一環です。

2. 暗号スイートのミスマッチ(TLSの壁)

ZTNAゲートウェイとレガシーアプリの間、あるいはクライアントとゲートウェイの間で、古いSSL/TLSバージョン(TLS 1.0/1.1)や、脆弱な暗号化アルゴリズムを要求するレガシーシステムが存在します。

  • 原因: 最近のモダンなZTNAプロキシは、デフォルトでセキュアなTLS 1.2 / 1.3しか許可していないため、古いクライアントやサーバーが弾かれます。
  • 対策: セキュリティリスクを最小限に抑えつつ、一時的にプロキシのバックエンド側でレガシーな暗号スイートを許可する、あるいは極力プロキシの手前側でモダンな通信にラップし直す設計(トランスレーション)を検討します。

3. アイデンティティと監査ログ(誰が何をしたか)の紐付け

従来のVPNや直接接続では、「誰がその踏み台からSSHログインしたのか」が個人の秘密鍵の管理に依存しており、監査が非常に困難でした。

  • 対策: ZTNAプロキシを導入する最大のメリットは、「誰が(User ID)、どのデバイスから(Device Posture)、いつ、どのレガシーサーバーにアクセスしたか」をセッション単位で完全にログに残せる点です。プロキシのアクセスログと、バックエンド側のログインログ(auditdやWindowsイベントログ)のタイムスタンプを突合できるように、NTPによる時刻同期は絶対に狂わせないでください。

—

まとめ

レガシーアプリケーションのZTNA収容は、一見すると「モダンなゼロトラストの思想に逆行する面倒な作業」に見えるかもしれません。しかし、すべてのシステムを明日一斉にクラウドネイティブなWebAPIに書き換えることなど不可能です。

「動くレガシーはそのままに、その周囲をセキュアなプロキシと厳格なアイデンティティ検証の壁で覆う」。
この現実的かつ泥臭いアプローチこそが、企業全体のセキュリティポスチャを確実に引き上げる王道です。

あなたのインフラでも、野良で放置されているレガシーなTCPポートがないか、今一度ネットワークの棚卸しをしてみてはいかがでしょうか? 次のインシデントを防ぐのは、間違いなくこうした地道なプロキシ設計の積み重ねです。

コメント

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