TCP serverをSSL/TLS化するのに nginx の stream_ssl_module/stream_proxy_module が便利
最近 Fluentd の通信プロトコルまわりをアップデートするためにあれこれいじっている*1んだけど、これはおおむね fluent-plugin-secure-forward がサポートしていた内容を Fluentd 組込みの forward plugin でもサポートしますよ、というものになる。
んで問題なのが secure-forward は SSL/TLS での接続のみしかサポートしてなかったんだけど forward では生の TCP で通信する*2ので、本当に secure-forward と forward それぞれの実装間で互換性が保たれているのか、直接的には確認する手段がない、ということになってしまう。
TCP server の SSL/TLS 化
一方世の中には SSL/TLS ターミネータという機能があって、たとえばロードバランサなんかがこの機能を持っている。何をやるかというとSSL/TLSの処理だけはフロントエンドのサーバがやって、バックエンドで実際の処理を担当するサーバはSSL/TLSのことなんか知らず生のTCPに対して処理を行う、というものだ。Webサービスの提供をするときにもこの構成はよく使われるし、AWS ELBなどをはじめ各種ロードバランサで広くサポートされている。
[client] -----> (SSL/TLS) -----> [Load Balancer] -----> (TCP) -----> [server]
これは Fluentd においては次の構成にあたる。
[out_secure_forward] -----> (SSL/TLS) -----> [Load Balancer] -----> (TCP) -----> [in_forward]
TCP client の SSL/TLS 化
いっぽう、本番システムにはあまり存在しない構成として、SSL/TLSに対応していないクライアントを無理矢理にでも SSL/TLS のみしかサポートしないサーバと通信させるたい、という要望がある。おそらくほとんどのユースケースは開発時のものだと思う。強いて言うと tcpdump で中身がわかって便利、みたいな SSL/TLS に正面から挑戦するような利点がある。
[client] -----> (TCP) -----> [Load Balancer] -----> (SSL/TLS) -----> [server]
Fluentd においてはさっきと真逆の構成。
[out_forward] -----> (TCP) -----> [Load Balancer] -----> (SSL/TLS) -----> [in_secure_forward]
プラグイン実装の対称性が保たれているかを確認するためにはこっちのテストも必要だったのです。
nginx の stream_ssl_module と stream_proxy_module
さてこのような構成を手元で作って動作確認したいと思ったときにどうするか。本番システムなら AWS ELB とか使えばいいけどちょっと動作を試したいとかオレオレ証明書とかであれこれ実験したいとかいうときにはいかにも面倒くさい。
おっさんとしては昔ながらの安定のツール stone とかも思い付くけど、うーん、さすがにちょっと今更感がある*3し、英語が大前提のOSSプロジェクトではオススメしづらい。
どうしようかなと思っていたら、最近の nginx はTCPのproxyもできる stream という機能が足されてたのを思い出した。自分でちゃんと使ったことがなかったんだけど、ドキュメントを見ているといかにも SSL/TLS の処理もできそうに書かれてていかにも使えそうだったので、試してみたところ、見事に(簡単に!)使えた。期待通りに動くソフトウェアというのはすばらしい。えらい。
なお stream およびその他今回使用したモジュールは通常の nginx では有効になっていない。自分は --with-stream --with-stream_ssl_module を有効にしてソースコードから configure && make && make install した。バージョンは最新安定板の 1.10.1 を使っている。
stream_ssl_module
https://nginx.org/en/docs/stream/ngx_stream_ssl_module.html
これは SSL/TLS なクライアントからの通信において TLS termination を nginx で行い、TCPの通信としてバックエンドに流すために使う。つまり、こっちの構成用。
[out_secure_forward] -----> (SSL/TLS) -----> [Load Balancer] -----> (TCP) -----> [in_forward]
もちろん手元で適当なオレオレ証明書を事前に用意しておく。その上で以下のように設定を書くだけ。簡単!
worker_processes auto; error_log /Users/tagomoris/nginx.err; events { worker_connections 64; } stream { upstream backend { server 127.0.0.1:24224; } server { listen 24284 ssl; proxy_pass backend; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers AES128-SHA:AES256-SHA:RC4-SHA:DES-CBC3-SHA:RC4-MD5; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; ssl_password_file /path/to/pass.txt; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; } }
port 24284 を SSL 有効にして listen し、その通信をバックエンドの Fluentd port 24224 に流す。こっちはTCP。
手元テスト用なので ssl 関連の設定は適当、本番環境にこれをコピペしてはいけない……のはともかく、これで動く。素晴らしい。
stream_proxy_module
https://nginx.org/en/docs/stream/ngx_stream_proxy_module.html#proxy_ssl
こちらは TCP で受けた通信を SSL/TLS が有効なサーバに向けてプロキシしてやるために使う。こっちの構成用。
[out_forward] -----> (TCP) -----> [Load Balancer] -----> (SSL/TLS) -----> [in_secure_forward]
こっちは証明書まわりの設定がいらない*4から、もっと簡単。こんな。
worker_processes auto; error_log /Users/tagomoris/nginx.err; events { worker_connections 64; } stream { upstream backend { server 127.0.0.1:24284; } server { listen 24224; proxy_pass backend; proxy_ssl on; } }
TCP port 24224 で受けた通信をバックエンドの port 24284 に流す、その際に SSL を有効にする、と指定してやるだけ。超簡単。
このとき証明書の検証まわりの制御について設定が必要なら、適宜 proxy_ssl_verify などを正しく指定してやればいい。各自がんばれ。
まとめ
nginx の stream がすばらしくて、なんといっても手元で使いやすいので開発時にも特に便利にお使いいただけます、というものになってた。
今後の選択肢のひとつとして覚えておきたい。