Amazon Athena を使ったセキュリティログ検索基盤の構築

こんにちは。技術部セキュリティグループの水谷(@m_mizutani )です。最近はFGOで一番好きな話がアニメ化され、毎週感涙に咽びながら視聴しています。

TL;DR

  • これまでセキュリティログ検索にGraylogを使っていたが、主に費用対効果の改善のため新しいセキュリティログ検索基盤を検討した
  • 自分たちの要件を整理し、Amazon Athenaを利用した独自のセキュリティログ検索基盤を構築した
  • まだ完全に移行はできていないが対象ログを1ヶ月間分(約7.5TB1)保持してもコストは1/10以下である3万円に収まる見込み

はじめに

セキュリティグループでは日頃、社内ネットワークやPC環境、クラウドサービスに関連するセキュリティアラートに対応するセキュリティ監視業務を継続しておこなっています。アラートに対応する時に頼りになるのはやはり様々なサービスやシステムのログで、そのアラートに関連したログを調べることにより誰が、いつ、どのようなコンテキストでそのアラートに関わったのかということを知ることができ、アラートのリスク評価において大きな役割を担っています。

一方でこのようなセキュリティ監視のためのログは大量になるため検索をできるようにするための基盤を整えるのも簡単ではありません。クックパッドのセキュリティグループではこれまでGraylogを使ってセキュリティ監視のためのログ検索基盤を構築・運用してきましたが、運用していく中でいくつかの課題が浮き彫りになったため、現在はAmazon Athenaを使ったログ検索基盤への移行を進めています。本記事では開発・移行を進めている新セキュリティログ検索基盤について解説します。

Graylogを利用する際の課題

過去に本ブログでも記事として紹介しましたが、クックパッドでは様々なログをセキュリティ監視のために取り込むことによって、アラート発生時に横断的な調査ができるようになっています。今日の企業における活動というのは1つの情報システムやサービスだけで完結するということはほとんどなく、複数のシステム・サービスをまたがって遂行されます。そのためアラートがあったときに全体像の把握をするためには複数種類のログを横断的に検索できるGraylogは非常に有用でした。しかし運用を続ける中で以下のような課題もでてきました。

  1. 弾力性があまり高くない:クックパッドで利用しているGraylogのバックエンドはAmazon Elasticsearch Serviceを利用しており、データノードをスケールイン・スケールアウトする機能が備わっており、データノードの負荷増大やディスク空き容量の減少に対して簡単に対応できます。ただ、スケールイン・スケールアウトは瞬時に実施できるわけではなく、経験則としては数分〜数十分の時間を要します。そのため、急なログ流量の増加のスムーズに対応するのは難しく、ログの取りこぼしなどが発生する可能性と向き合う必要があります。
  2. 使用頻度に対してコストが高い:弾力性の問題に対して余裕を持ったリソースを常時運用するという方法もありますが、今度はコストが問題になってきます。Graylogのバックエンドとして利用するElasticsearchはそれなりにリソースが必要となるため、利用するインスタンスの性能やディスクの容量を大きくとらなくてはなりません。現在クックパッドで運用しているGraylogでは一日あたり合計250GBのログを受け取り、ログの種類に応じて保管期間を最大1ヶ月、流量が多いものは1週間程度に収めることで、およそ月額40万円強、年間500万円ほどのコストをかけています。Graylogはインタラクティブかつ高速に検索が可能であるため、業務時間中は常にセキュリティ分析のために検索をしているような状況であればコストに見合ったメリットがあると考えられます。しかし、(幸いにも)クックパッド内で発生するセキュリティアラートは平均して日に2〜3回程度であるため、そのために年間500万円もかけるのはあまり効率的ではない、と考えています。
  3. Elasticsearchの運用が辛い:先述したとおりクックパッドで利用しているGraylogのバックエンドはAmazon Elasticsearch Serviceなので、自らでインスタンスを立てて運用するのに比べると比較的楽ですが、それでも負担はそれなりにあります。先述したとおりかなりの流量のログを投入するため、流量が想定を超えるとログの投入だけでなく検索にも影響がでます。また、様々な種類のログを投入するため、ログのスキーマや内容によって過大な負荷がかかるということがしばしばありました。そのため、それほど流量が多くないログでも投入に慎重にならざるをえず、またログの種類によってはそもそも取り込むのが難しいというようなこともありました。

これらの問題を解決するために、AWSのオブジェクトストレージであるS3をベースにした検索基盤が作れないか、ということをかれこれ1年くらい模索していました。

「セキュリティ監視」におけるログ検索の要件

具体的にどのようなアーキテクチャを採用したのかを説明するために、セキュリティ監視という業務をする上でのログ検索の要件を整理します。具体的には以下の5点になります。

  1. 複数種類のログに対してスキーマに依存しない検索ができる
  2. ニアリアルタイムで検索ができる
  3. 単語を識別した検索ができる
  4. ログの投入時に容易にかつ迅速にスケールアウト・スケールインが可能である
  5. 全体的な費用負担を減らす

Graylogはこれらの要件のうち1、2、3は十分に満たしていましたが、4、5の部分が期待する水準ではなかったと言えます。以下、各要件を詳しく解説します。

(要件1) 複数種類のログに対してスキーマに依存しない検索ができる

セキュリティ監視という業務の特性による最も重要な要件がこれだと考えています。通常、データ分析をしたい場合はデータごとに決まっているスキーマを理解し、そのスキーマに合わせたクエリを発行すると思います。一方でセキュリティ監視においては「あるキーワード(IPアドレス、ドメイン名、ユーザ名など)がどのフィールドに含まれるかを気にせず一気に検索する」というユースケースが圧倒的に多くなります。ログを絞り込んでいく上でフィールドを指定する必要はありますが、まず全文検索のような形でログを検索してどういった種類のログがどういった傾向で出現しているかという全体像を把握することが大切になってきます。

ログの種類が2〜3種類のみであればまだ人間がスキーマを覚えてクエリを作る事ができるかもしれませんが、数十種類になってくると人間がこれを覚えてクエリを作るのはあまり現実的ではありません。また、コード上でスキーマを管理してクエリを自動生成するというような方法も考えられますが、今度はメンテナンスが面倒になってきます(3rd partyサービスのログのスキーマがいきなり変わることはしばしばあります)そのため、ログのスキーマを全く気にせずに "10.1.2.3" というキーワードが含まれるログをバシッと検索できる仕組みが必要になります。

(要件2) ニアリアルタイムで検索ができる

セキュリティアラートが発生した場合、なるべく迅速に分析をできる状態になっていることが望ましいです。具体的なリアルタイム性(どのくらいの遅延を許容できるか)については一般的なManaged Security Service(MSS)のSLAが参考になります。いくつかのMSSではおよそ15分以内にアラートに関する第一報を報告するよう定められています。

これを基準とした場合、分析などに5〜10分ほどかかることを考えるとログが到着してからできれば5分以内、遅くても10分以内には分析ができる状態になっているのが望ましいと言えるでしょう。完全なリアルタイム性を実現する必要はありませんが、到着したログが逐次検索可能になるようなパイプラインは必要かと考えられます。

(要件3) 単語境界を識別した検索ができる

これは非常に地味ですが、意外と大切な要件だと考えています。検索する際に "10.1.2.3" を指定したら、 "110.1.2.3" や "10.1.2.30" を含む全てのログではなく、"10.1.2.3" のみが検索結果に出てくるようにするべきだと考えています。これはJSONなどの構造化データにおいて1つのフィールドに1つの単語しか入らない、ということが定まっているログについてはあまり悩む必要がありません。しかしsyslog由来のログやアプリケーションから出力されるログは自然言語のように記述されたログが頻出するため、ログ全文に対する単純な文字列マッチでは実現が難しいです。

単純な文字列マッチだけでなく、正規表現などを利用して前後の区切り文字などを排除するという方法もあることにはあります。ただその場合だと利用する人間がオリジナルのログの構文や構造などを強く意識してクエリを作成しなくてはならず、さらにトライ&エラーが発生するのを前提としてしまうため、あまり望ましくありません。さらに人間だけでなく別のシステムから自動で検索をかけたいと思った場合にトライ&エラーは期待できないため、一発で期待する検索ができることが望ましいです。

(要件4) ログの投入時に容易にかつ迅速にスケールアウト・スケールインが可能である

Graylog運用における課題1で述べたとおりですが、ログの種類が増えたり、ログの種類が同じでも流量が増えた際に速やかにスケールアウトし、負荷が低くなったらスケールインできる仕組みが求められます。場合によってはかなり頻繁にログの種類が追加されるということもあるため、その都度事前に流量や負荷の度合いを計算して準備をする、といった煩わしさがないような仕組みが望ましいと言えます。

(要件5) 全体的な費用負担を減らす

これもすでにGraylog運用における課題の2で述べたとおりですが、なるべく費用の負担が小さいに越したことはありません。特にセキュリティ監視という領域はいざというときに事業を守るために必要ではあるものの、お金をかけるほどビジネスが加速する、というものではないので費用を抑えられるに越したことはありません。

ログ検索のためのアプローチ

ここまでで説明させてもらった要件を満たす基盤を構築するため、以下の2つの方針を考えました。

(1) ログはAWS S3に保存して検索はAthenaを利用する

AWSのS3はオンラインストレージ(例えばEC2にアタッチされたEBSなど)と比較して非常に安価に巨大なデータを保持することができます。課題の部分でも説明させてもらったとおり、1日中高頻度に検索をするというシステムには向かないかもしれませんが、低頻度にアクセスするデータを保持しておくには最適な選択肢の一つだと考えられます。これによって大量のログデータを保持する場合でも全体的な費用負担を抑えることができます。

S3に保存したデータから必要となるログを検索するには、自分でSDKを使いS3からオブジェクトをダウンロードするようなコードを書く、S3 Selectを使う、Redshift Spectrumを使う、などいくつかの選択肢が用意されていますが、今回はクエリは低頻度であること+ある程度複雑なクエリが発生することなどを踏まえ、Amazon Athenaを利用することにしました。S3 selectは基本的に文字列のフィルタのみになるので、複雑な条件を指定するような検索には不向きになります。また、Redshift Spectrumは複雑なクエリを扱えますがクラスタを構築して常時稼働させるのが前提となってしまうため、低頻度にしか利用しないという今回のユースケースでは過剰に料金がかかってしまいます。Amazon Athenaは通常のSQLと同等のクエリが記述でき、さらにクエリによって読み取ったデータの量に応じて課金されるため、今回のユースケースに適していました。

(2) ログの投入時にLambdaを利用してインデックスを作成する

S3 + Athenaを使うことで費用面に関する問題は解決できますが、一方でログをそのまま保存していただけでは「スキーマに依存しない検索ができる」という課題を解決できません。AthenaはSQL形式のクエリを発行して指定したフィールドの値、あるいは集計結果を取得するため、そのままAthenaで横断的なキーワード検索をしようとすると全てのフィールドに対して検索をかけるという無茶が必要になってしまいます。

そこで、ログを保存用S3バケットに投入する際に元のログだけではなく、ログからインデックスを作成し、それをAthenaで検索できるようにします。全てのログを投入前に一度パースして辞書型に変換し、そのキーと値をもとにインデックスを作成します。例えばJSON形式で {"src_addr":"192.168.0.1", "dst_addr": "10.1.2.3"} というログがあったら、("src_addr", "192.168.0.1"), ("dst_addr", "10.1.2.3") という2つのレコードを作成し、それぞれにS3オブジェクトのIDや行番号などを付与します。これをインデックスとしてAthenaで扱うテーブルの1つとして作成します(Indexテーブル)。さらにログ本文とオブジェクトのID、行番号を含むテーブルも作成します(Messageテーブル)。イメージとして以下のようなテーブルをそれぞれ作成します。Object IDはただのカウンタであり、行番号はそのオブジェクト内で何番目にでてくるログなのかを示しています。最終的にはMessageテーブルの「ログメッセージ本文」が検索結果として返され、セキュリティ分析をする際に参照することになります。

f:id:mztnex:20191118230126p:plain

このようなテーブルを作成することで、全てのログから特定の値をもつフィールドを検索したり、あるいは特定のフィールドにのみ含まれる値をIndexテーブルから探すことができます。この結果をMessageテーブルと結合することで、特定の値を含むログをスキーマに依存せず探し出すことができるようになり、スキーマに依存しない検索が実現できます。

また、Lambdaを使ってインデックスの作成をすることで容易にスケールイン・アウトができるようになります。Lambdaは特別な設定をすることなく、タスクの増減に応じて同時実行数がシームレスに増えていきます。そのためログの流量が増えたとしても事前にスケールアウトするなどの準備も必要なく、AutoScalingが間に合わず処理の遅延やログの消失が発生する、というようなことも回避できます。また、ログ発生のイベントを受け取りLambdaで処理してS3に投入するというパイプラインによって、検索可能になるまでの遅延もおよそ1分程度に抑えることができ、セキュリティアラートが発生したあとログ検索ができなくて待たされるという問題が解決できます。

ログ検索基盤の設計と実装

f:id:mztnex:20191118230147p:plain

アプローチの節で説明したものを実装したアーキテクチャが上の図になります。実装名は Minerva(ミネルヴァ) と呼んでいます2。今回は原則としてサーバーレスで実装しており、点線内のS3バケット以外はCloudFormationでデプロイしています。これによって、バージョン変更時にstaging用の環境を作りたいと思ったら新しい設定ファイルを用意すればコマンド一発で真新しい環境を作ったり逆に不要になった環境を削除できます。Web UIについてもECSのspot instance上(参考記事)で動かしています。

このアーキテクチャは主に (1)ログの投入、(2)パーティションの作成、(3)ログのマージ、(4)ログの検索、という4つのパートに別れて構成されています3。それぞれ解説したいと思います。

f:id:mztnex:20191118230159p:plain

(パート1) ログの投入

ここまでの説明ではわかりやすさのために「ログが生成されたらそのままインデックスが生成される」というような書き方をしていましたが、実際にはログが本来保存されるS3に投入される → そのイベントをSNS+SQSで流してLambdaを起動する → Lambdaが対象のオブジェクトをダウンロード&インデックス作成をした後にMinerva用のS3バケットに投入し直す、ということをやっています。これはログの保全という観点から、まずはログをS3バケットに格納して保全できる状態にし、そこからいろいろな処理にパイプラインをつなげて可用性を高める、という考え方に基づくものです。これによってインデックス作成の処理が失敗したとしても、元のS3バケットを再度参照することで容易にリトライが可能になります。

先述の通り、処理にはLambdaを使うことでエンジニアが気にしなくてもスケールイン・アウトが可能となっています。実際には事故を防ぐために最大同時実行数を制限して運用していますが、最大同時実行数を多めに設定してもそれ自体に課金はされないので、計算した分だけの料金ですみます。

またログ検索時に本文中の単語を適切に識別して検索ができるように、Lambdaでパースをした際に単語を記号や空白と言った文字で分解したものをインデックスとして登録するようにしています。これはElasticsearchにおけるStandard Tokenizerに近い、トークン分割の独自実装を利用しています。例えば tani@cookpad.com というような文字列は tani, cookpad, com に分解してインデックスを作成し、検索する時にはこれらの単語を完全一致で全て含むログを検索するようにします。これによって tani@cookpad.com を検索したいときに mizutani@cookpad.com も引っかかってややこしい、という状況を回避できます。(もちろん、意図的に %tani を指定することで、mizutani@cookpad.com も引っかかるようにできます)

(パート2) パーティションの作成

次のパートはAthenaのテーブルのパーティション作成になります。Athenaは読み込んだS3オブジェクトのサイズの合計のみで課金が決まるため、少しでも不要な読み込みを減らすのがパフォーマンスおよびコストにとって重要になってきます。この読み取りサイズを減らす一つの方法として有効なのがパーティション作成です。S3のパスのプレフィックスをパーティションとして登録しておき、WHERE節でそのパーティションを指定するように条件を記述すれば、他のプレフィックスが読み込まれなくなり、読み込むデータサイズを大幅に削減できます。

例えばMinervaでは以下のようなフォーマットで変換したオブジェクトを保存しています。保存先のバケット名が ***-bucket です。

s3://***-bucket/some-prefix/indices/dt=2019-11-01-05/some-bucket/some-key.parquet

s3://***-bucket/some-prefix/indices まではただのプレフィックスですが、dt=2019-11-01-05 がパーティションを示すためのパスの一部になります。このパスの dt=2019-11-01-05(2019年11月1日5時の意味)をAthenaに登録しておくと、 WHERE dt = '2019-11-01-05' とすることによって、上記のプレフィックスを持つオブジェクトしかデータが読み込まれなくなります。これは '2019-11-01-04' <= dt AND dt <= '2019-11-01-06' と指定すると、4時から7時前(6時代)までを検索対象とできます。まず短い時間の範囲から検索したい単語などを探し、検索したい単語が見つからなかったりより深く対象の行動を追いたいということがあれば、その後必要に応じてより長い時間の範囲を検索していくことで、必要最低限のデータ読み込みですむ(=料金も安くてすむ)という使い方を想定しています。

(パート3) ログのマージ

IndexテーブルやMessageテーブルに投入されたオブジェクトはそのままでも検索はできるのですが、検索のパフォーマンス(クエリ速度)が悪化するという問題があります。Athenaのパフォーマンスチューニングのベストプラクティスによると128MB以下のファイルが多いとオーバーヘッドがでてしまうとあるのですが、現在のクックパッド内の環境だとログとして保存する際にある程度の大きさになるオブジェクトもあれば、完全に細切れで保存されるオブジェクトもあり、特に細切れのオブジェクトはパフォーマンスに影響を与えてしまう可能性が高いです。

そのため、一定時間ごと(現在だと1時間毎)にCloudWatch Eventを発火させて、マージすべきオブジェクトの一覧作成&どのようにマージすべきかの戦略をlistParquetというLambdaが判断し、mergeParquetというLambdaが複数のオブジェクトをマージするという作業をしています。これによって1日数十万単位で作成されていたオブジェクトを1000個程度に抑えることができるようになりました。

(パート4) ログの検索

最後は実際のログの検索です。ログの検索は当然Athenaに直接SQLを送っても結果を得られるのですが、「(1)ログの投入」パートで説明したとおり、検索対象を単語で分割するようなやや複雑なクエリを使う必要があります。これを利用する側が意識せずによりシンプルなAPIとして使えるようにAPI gatewayを通してリクエストできるようにしています。これは、この検索基盤をエンジニアがWeb UIを通して使うだけではなく、将来的に他の社内セキュリティシステムとも連動させたいと考えており、その際にSQL文構築の知識を分散させずにMinervaの中に閉じ込めておきたい、という意図もあります。

Web UIについてはまだ開発中ではありますが、現在は以下のようなシンプルなインターフェイスで動かせるよう開発を続けています。

f:id:mztnex:20191118230849p:plain

本アーキテクチャによる効果

先程の述べたとおりまだ開発中のものもありMinervaに完全に移行できたわけではないのですが、当初に考えていたコスト削減については大きな効果が期待できそうです。前述したとおり、現在クックパッドのGraylogはログの保存期間を流量に応じて1週間〜1ヶ月と調整しながら使っていて月に40万円以上かかっていますが、現在の開発しながら使っているMinervaのコストは全てのログを1ヶ月間(未圧縮で約250GB/日 x 30日=約7.5TB)保持しても月あたり3万円以下に収まっています。これによって当初の目的であったコストの抑制には大きく貢献できる見通しがたちました。

今後の課題

コストの面ではかなり大きな成果をあげられそうなMinervaですが、まだ開発における課題は以下のようなものが残っており引き続き開発を進めていきたいと考えています。

  • インデックス作成に関する計算パフォーマンスを向上させる:このシステムはインデックス作成が肝になっており、実はかかっている料金の半分以上がインデックス作成+マージのための計算コストになっています。現在は列指向ファイルの恩恵をうけるためにParquet形式を利用していますが、そのためのオブジェクト生成のためのCPUおよびメモリが必要になっており、これがコストに影響しています。そのため、より効率の良い方法でparquetに変換できるようにすることで、さらにコストを抑制できる可能性があり、コストを抑制できればさらに多くのログを長期間保存しやすくなるため、引き続き改良を続けていきたいと考えています。
  • 検索時のパフォーマンスを向上させる:当初から「検索時のクエリがGraylogより遅くなることは許容する」という前提で開発をしていたため、クエリ結果が返ってくるのが遅いという点については許容するものの、やはり早く応答が返ってくるに越したことはありません。現在だとやや複雑なクエリを投げているせいもあり、およそ20秒程度クエリに時間がかかってしまっています。一度結果が取得できればその結果からの絞り込みなどはできるようにしていますが、なるべくインタラクティブな検索を実現したいと考えています。そのためデータ構造やAthenaの使い方について、より検討を進めたいと考えています。

最後に

クックパッドではこうした「エンジニアリングでセキュリティの問題を解決する」ことを一緒にやっていける仲間を募集しています。興味のある方はぜひこちらをご参照いただくか、ご質問などあれば水谷(@m_mizutani)などまでお声がけください!


  1. 7.5TBという数字はあくまでもとのログの非圧縮状態でのデータサイズです。実際に保存される際はトークン分割などの変換や圧縮がかけられるので単純計算はできませんが、実測値でおよそ1/3〜1/4程度になります。

  2. これは決して厨二病を発症したわけではなく、Athenaの元々の意味であるギリシア神話の女神アテナの別名がミネルヴァとのことだったので、Athenaを少し変わった使い方をすることからこのような名前にしました。

  3. 今回のセキュリティログ検索基盤は弊社で別途運用されているデータ活用基盤のprismを大いに参考にさせてもらっています。詳しくはこちらの記事もご参照ください

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/