fluent-plugin-secure-forwardと戯れた話
こちらのエントリーは「GREE Advent Calendar 2015 14日目」の記事です。
こんにちは、データエンジニアリングチームの山田です。6日目の記事の長谷川、田畠とともにグリーの分析基盤を良くする仕事をしています。主にデータ収集周りを担当しており、fluentdとはいつの間にか2年を超える付き合いとなりました。
さて今回は、fluent-plugin-secure-forwardについてつらつらと紹介していきたいと思います。
fluent-plugin-secure-forward(以下secure-forward)は、SSL/TLSを利用しセキュアにデータ転送を行うためのfluentd pluginです。fluentdにデフォルトで含まれているノード間を転送するためのforward plugin(以下forward)は転送時に暗号化などを行っていないため、インターネットを通しての転送には傍受の危険性などがあります。しかし、secure-forwardを使うことでセキュアに転送することが可能です。secure-forwardはいくつかの認証/制限機能を提供しており、今回のエントリでは特にこのあたりの機能と、検証を進めた際にハマったことを中心に紹介していきます。このエントリがこれからsecure-forward、はたまたfluentdを導入しようかな?と考えている方々の手助けになれば幸いです。
それでは、さっそくsecure-forwardの導入から設定ファイルを交えながら認証/制限機能を紹介していきます。
fluent-plugin-secure-forwardのインストール
fluentd自体のインストールは環境によって少し異なりますので、公式のドキュメントを参考にしてインストールしてください。
fluentdのインストールが完了したら、secure-forwardをインストールします。
1 2 3 4 5 |
# fluentdユーザ向け fleunt-gem install fluent-plugin-secure-forward # td-agentユーザ向け td-agent-gem install fluent-plugin-secure-forward |
sudoに関しては適宜追加してください。インストール方法はほかのfluent-pluginと同じです。簡単ですね。: )
設定例と認証方法いろいろ
いきなりですが、まずはイメージしやすいように設定例をお見せします。その後各設定について解説していきます。
設定例
以下では、secure-forwardでデータを受け取る側のノードをアグリゲータ、データを転送する側のノードをクライアントと呼ぶことにします。この設定例を図で表すと以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<source> type secure_forward bind <%= AGGREGATOR_IP_ADDRESS %> self_hostname <%= HOST_NAME %> secure yes # 共通キー shared_key XXXX # 証明書 ca_cert_path /path/to/ca_cert.pem ca_private_key_path /path/ca_key.pem ca_private_key_passphrase YYYY # ユーザ認証する authentication true # IP制限する allow_anonymous_source false # 許可するユーザ <user> username yuki password **** </user> <user> username takuya password **** </user> # 許可するクライアントのIPアドレス、IPレンジ # クライアントによっては許可するユーザを限定可能 <client> host xxx.xxx.0.10 </client> <client> network xxx.xxx.1.0/24 </client> </source> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<match test> type secure_forward self_hostname <%= CLIENT_HOST_NAME %> secure yes # 共通キー shared_key XXXX # 証明書 ca_cert_path /path/to/ca_cert.pem # 転送先サーバ # 転送先毎にユーザ名やパスワード、shared_keyを指定できる <server> host <%= AGGREGATOR1_IP_ADDRESS %> user yuki password **** </server> <server> host <%= AGGREGATOR2_IP_ADDRESS %> user yuki password **** </server> buffer_type file buffer_path /var/log/td-agent/buffer/sec_forward flush_interval 10s </match> |
少しごちゃごちゃとしてしまいましたが、行っている設定と設定例の内容をざっくりまとめると以下のとおりです。
アグリゲータ(受け取る側)
- 証明書のパスとパスフレーズの設定
shared_key
を設定- 接続してくるクライアントが提示する共通キーが
XXXX
な場合のみ許可する
- 接続してくるクライアントが提示する共通キーが
<user>
によって許可するユーザを指定- 接続してくるクライアントが提示するユーザが
yuki
かtakuya
でありパスワードが一致してる場合のみ許可する
- 接続してくるクライアントが提示するユーザが
<client>
によってIP制限や、クライアント毎に転送を許可するユーザを指定可能xxx.xxx.xxx.0.10
とxxx.xxx.1.0/24
なクライアントからの接続要求だけを許可する
クライアント(転送する側)
- 証明書のパスを設定
shared_key
を設定- アグリゲータへの接続時に提示する共通キーは
XXXX
- アグリゲータへの接続時に提示する共通キーは
<server>
によって転送先、ユーザ情報を指定- 転送先は
AGGREGATOR1_IP_ADDRESS
,AGGREGATOR2_IP_ADDRESS
- 接続時に提示するユーザは
yuki
- 転送先は
これだけの設定で共通キー、ユーザによる認証と、IP制限が可能です。
なお、secure-forwardの転送側であるout_secure-forwardは、fluentdにデフォルトで含まれているout_forwardと同じObjectBufferedOutputですので、バッファ周りや転送するまでの間隔の設定は同じように行うことができるので環境に合わせて設定してください。
認証方法いろいろ
それでは各設定についてもう少し詳しく紹介していきます。
SSL/TLS通信を行うために
まずはSSL/TLS通信を行うために必要な証明書周りの設定について紹介します。secure-forwardでは信頼できるCAを指定することも可能ですが、今回はプライベートな証明書を作成して利用する方法を紹介します。
プライベートなCA cert/keyの作成
1 2 3 4 5 |
# fluentdユーザ向け secure-forward-ca-generate <%= KEY_PATH %> "<%= PASSPHRASE %>" # td-agentユーザ向け /opt/td-agent/embedded/bin/secure-forward-ca-generate <%= KEY_PATH %> "<%= PASSPHRASE %>" |
これでKEY_PATH
以下にca_cert.pem
, ca_key.pem
が作成されているはずです。
証明書周りの設定
先ほど作成したファイルのうち必要なものを各サーバに配り、ファイルパスとパスフレーズを指定します。
1 2 3 |
ca_cert_path /path/to/ca_cert.pem ca_private_key_path /path/to/ca_key.pem ca_private_key_passphrase XXXX |
1 |
ca_cert_path /path/to/ca_cert.pem |
この設定は必須項目です。
なお、この証明書は5年間で期限が切れます。
shared_key
クライアント、アグリゲータで同じ文字列を指定します。これが一致していない場合転送することができません。ユーザ認証などのほかの認証方法はオプションなのですが、このshared_key
は必須項目なので注意が必要です。また、<client>
, <server>
ディレクティブにて転送元(先)毎にshared_key
を個別に設定することも可能で、この指定方法をとるとディレクティブ内のshared_key
による認証が優先されます。
1 2 3 4 5 6 7 8 9 10 11 12 |
# client内で指定していてもこれは必須 shared_key hoge # host xxx.xxx.xxx.xxx からの接続時はshared_key = fuga <client> host xxx.xxx.xxx.xxx shared_key fuga <client> # host yyy.yyy.yyy.yyy からの接続時はshared_key = hoge <client> host yyy.yyy.yyy.yyy </client> |
1 2 3 4 5 6 7 8 9 10 11 12 |
# client内で指定していてもこれは必須 shared_key hoge # host aaa.aaa.aaa.aaa への接続時はshared_key = fuga <server> host aaa.aaa.aaa.aaa shared_key fuga </server> # host bbb.bbb.bbb.bbb への接続時はshared_key = hoge <server> host bbb.bbb.bbb.bbb </server> |
shared_key
が違っていた場合には、アグリゲータ側に以下の様なメッセージが流れます。
1 |
[warn]: Shared key mismatch from 'HOST_NAME' |
ユーザ認証
ユーザ認証を有効にするためには、アグリゲータ側で authentication true
にする必要があります(デフォルトではfalse
)。ユーザの情報はアグリゲータ側では<user>
で、クライアント側では<server>
内で指定します。また、クライアント毎に許可するユーザを絞ることも可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# ユーザ認証を行うためには必須 authentication true # ユーザ情報を列挙 # <client>で特にIP制限の指定がない場合、ここのユーザからの接続だったら許可する <user> username yuki password **** </user> <user> username takuya password **** </user> <user> username taro password **** </user> # host xxx.xxx.xxx.xxx からの接続時は ユーザ yukiとtakuya のみ許可する <client> host xxx.xxx.xxx.xxx users yuki,takuya <client> # host yyy.yyy.yyy.yyy からの接続時は <user>で指定されたユーザの場合許可する <client> host yyy.yyy.yyy.yyy </client> |
1 2 3 4 5 6 7 8 9 10 11 12 |
# ユーザ yuki として接続 <server> host aaa.aaa.aaa.aaa user yuki password **** </server> # ユーザ takuya として接続 <server> host bbb.bbb.bbb.bbb user takuya password **** </server> |
なお、アグリゲータ側で<user>
や<client>
内にユーザ情報を書いても、 authentication false
の場合は無視されます。
また、ユーザ認証が失敗した場合にはアグリゲータにて以下の様なメッセージが流れます。
1 |
[warn]: Authentication failed from client 'HOST_NAME', username 'USER_NAME' |
IP制限
IPによる制限をおこなうためには、アグリゲータ側で allow_anonymous_source false
にする必要があります(デフォルトではtrue
)。ホワイトリスト型の制限なので、許可するサーバ情報をアグリゲータ側に<client>
で指定します。
1 2 3 4 5 6 7 8 9 |
allow_anonymous_source false # 転送を許可するサーバもしくはIPレンジを列挙 <client> host xxx.xxx.xxx.xxx </client> <client> network yyy.yyy.yyy.0/24 </client> |
これらのサーバ以外からのアクセスが有った場合、
1 |
[warn]: Connection required from unknown host '***.***.***.***' (***.***.***.***), disconnecting... |
といったメッセージがアグリゲータに流れます。かなり簡単にIPアドレスによる制限ができます。
なお、アグリゲータ側でホワイトリストを設定しても、 allow_anonymous_source true
の場合は無視されます。ただし、IP制限は無効かつ、ユーザ認証が有効な場合には<client>
内の設定が一部効力を発揮するようなので注意が必要です。例えば以下の様な設定の場合、<client>
は無効化されておりIPアドレス xxx.xxx.xxx.xxx
のサーバ以外からのデータ転送を許可している状態にあります。しかし、IPアドレス xxx.xxx.xxx.xxx
なサーバからの転送に限り、<client>
内の users
と shared_key
を使用してユーザ認証を行おうとしてしまうため注意が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
authentication true allow_anonymous_source true shared_key XXXX <user> username yuki password **** </user> <user> username takuya password **** </user> # host xxx.xxx.xxx.xxxからの接続のみ許可する ← ただし無効化されている <client> host xxx.xxx.xxx.xxx users yuki shared_key YYYY </client> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# ケース1 # ユーザ takuyaだと転送できない <server> host aaa.aaa.aaa.aaa user takuya password **** </server> # ケース2 # shared_keyが<client>内のものと合わないので転送できない <server> host aaa.aaa.aaa.aaa user yuki password **** shared_key XXXX </server> |
以上で各認証方法の紹介は終わりです。
これらの認証に失敗した場合、fluentd自身がエラーメッセージをfluent.warn
タグをつけて流してくれるので、それをメールやチャットなどに投げるといろいろと捗るかもしれません。
ハマった点
さてここからは、検証を進めた際にハマったことをいくつか紹介していきます。
td-agentのバージョンによって利用できるSSL/TLSのバージョンが異なる
secure-forwardは ssl_version
オプションにて利用するSSL/TLSのバージョンを指定することができます。このオプションのデフォルトは TLSv1_2
なのですが、ruby1.9系を同梱しているtd-agent version1系ではこのデフォルトのバージョンを利用することはできません。
1 2 3 4 5 |
$ /opt/td-agent/embedded/bin/irb irb(main):001:0> require 'openssl' => true irb(main):003:0> OpenSSL::SSL::SSLContext::METHODS => [:TLSv1, :TLSv1_server, :TLSv1_client, :TLSv1_2, :TLSv1_2_server, :TLSv1_2_client, :TLSv1_1, :TLSv1_1_server, :TLSv1_1_client, :SSLv2, :SSLv2_server, :SSLv2_client, :SSLv3, :SSLv3_server, :SSLv3_client, :SSLv23, :SSLv23_server, :SSLv23_client] |
1 2 3 4 5 |
$ /usr/lib/fluent/ruby/bin/irb irb(main):001:0> require 'openssl' => true irb(main):002:0> OpenSSL::SSL::SSLContext::METHODS => [:TLSv1, :TLSv1_server, :TLSv1_client, :SSLv2, :SSLv2_server, :SSLv2_client, :SSLv3, :SSLv3_server, :SSLv3_client, :SSLv23, :SSLv23_server, :SSLv23_client] |
ssl_version
オプションでtd-agent version1が利用可能なSSL/TLSのバージョンを指定すればよいのですが、新しい機能もどんどん追加されていますしこの際、td-agentをversion2にあげてしまうのはどうでしょうか?
output pluginがスレッドセーフかどうか
通常のforwardは、インプットを担うスレッドは1つだけで、そのスレッドでバッファへの書き込みまでを行います。一方でsecure-forwardは、クライアントからのコネクション毎にスレッドが作成され、そのコネクションがスレッドとともに維持されます。つまりアグリゲータ側には、各クライアントに対して専用のインプットスレッドが立っている状態となります。これらのインプットスレッドは受け取ったデータをバッファへ書き込みます。バッファへ書き込む部分は排他制御されているのですが、受け取ったログを加工してバッファへ書き込むまでの部分に関しては各プラグインに委ねている部分が多く、弊社で独自に手を加えたプラグインがまさにこの部分で問題を起こしてしまいました。アウトプットプラグイン内でインスタンス変数をキャッシュのように利用していたため、複数のインプットスレッドによってレースコンディションが起きてしまいました。
今のところほかの公開されているプラグインでこうした問題が発生したプラグインを見たことはありませんが、独自に作成、拡張したプラグインを利用している場合には、念のためアグリゲータのアウトプットプラグインのemit
メソッドからformat
メソッドあたりまでがスレッドセーフかどうか確認してみてください。こうした振る舞いのテストを行う際に、fluentdの結合テスト的なことが気軽にできるツールが欲しいですね。
また、ハマったことではないのですがsecure-forwardが動いているアグリゲータに接続しているクライアント数には気をつけたほうが良いかもしれません。先述したように、secure-forwardのアグリゲータには接続してきているクライアント数だけスレッドが立つので、クライアント数がそのまま負荷に繋がる恐れがあります。そのため弊社では現在以下の図のように、webサーバなどから直接secure-forwardを使うのではなく、forwardで一度ログを集約してからsecure-forwardで転送するようにして、secure-forwardのアグリゲータに接続しているクライアント数をなるべく少なくなるようにしています。
こうした対策は、以前通常のforwardを利用していた際にアウトプット側のスレッドを多くしすぎたために「can't create thread」
なるwarnが出てfluentdが急に落ちたりとおかしな状態に陥ったことがあったため行っており、クライアント数の増加とともに今後も経過観察を続けていく予定です。
まとめ
fluent-plugin-secure-forwardの導入から各認証方法の設定を交えて紹介しました。どの設定も難しくないですし、ホワイトリスト型式でIP制限を行えるのは嬉しい機能です。
また、セキュリティが関わる部分なので、pluginのアップデートには追従したいところです。今年もfluentdにはたくさんの機能追加が行われていますし、td-agent/fluentdも積極的にバージョンアップしていきたいですね。 : )
明日は菊池さんによる記事です。菊池さんは、先月18日にオライリー社から書籍『OpenStack Swift』をリリースしています。ぜひショッピングカートに入れてあげてくださいね。お楽しみに!