Secrust — コアを増やしたら遅くなった話
Secrust を 1 週間かけて速くし、現実的なワークロードで 301% の改善を得ました。そのあと 並列度を 4 にしたら、スレッド 2 より 23% 遅くなりました。この結果は、どの最適化よりも私のエンジンに 対する見方を変えました。
これは機能一覧ではなく、判断の記録です。何のための道具か、何を選んだか、何を計測したか、何を間違えたか、 そして何を意図的に作らなかったか。
何の問題を解くのか
セキュリティテレメトリのシグナルは相関に宿ります。同一プリンシパルに対する多数の認証失敗のあとの 成功はアカウント乗っ取りです。この系列のどの単一イベントも疑わしくないので、イベント単位の検知器は これを見逃すか、環境内のあらゆるログイン失敗で発火するかのどちらかになります。
相関をきちんと扱うツール — イベント時刻、keyed state、ウィンドウ — は、たいていクラスタを前提にします。 Apache Flink はこれらの概念のリファレンス実装であり、本当に優れています。同時に JobManager、 TaskManager、JVM ヒープ、そして運用の物語が付いてきます。
だから私が答えを知りたかった問いは狭いものでした。
Secrust はその実験を出荷したものです。Sigma ルールを書き、OCSF イベントを流し込むと アラートが出ます。クラスタも JVM もブローカーも不要です。
何をするのか
4 つの検知戦略。セキュリティルールが実際に取る形をこれで覆えます。
- value match — ステートレス。1 イベント、1 述語
- counter — 一定時間内に N 件マッチ。任意のフィールドでグループ化可能
- window — タンブリングウィンドウ + count / distinct count トリガー
- correlation — 複数のベースルールが同一キーで一定期間内に発火。順序指定も可能
デモは 4 戦略すべてを網羅する 5 ファイルのルールパックに、仕込んだ攻撃を再生します。
git clone https://github.com/BlueSquadron/Secrust.git
cd Secrust
./scripts/demo.sh
lateral_movement がこのプロジェクト全体を正当化します。エンジンが代わりに下す判断
設定すべきスレッド数はなく、宣言すべきパーティションキーもありません。どちらもビルド時に コンパイル済みルールから導出されます。
これは私が最も強く擁護する設計判断で、理由は使い勝手ではありません。
user.name でグループ化した counter が正しく動くのは、あるユーザーのイベントすべてが、そのユーザーの
状態を保持しているワーカーに届く場合だけです。group_by フィールドでのハッシュ分割がそれを保証します。
もしパーティショニングが設定項目なら、ユーザーは誤った答えを設定できることになります。静かに、
エラーも出さず、単に低すぎるカウントを出しながら。
だからこれはノブではありません。ルールはどのフィールドが状態をグループ化するかを既に述べているので、 エンジンはそこからパーティション計画を読み取ります。value match ルールは状態を必要としないので、投入 スレッド上でインライン評価され、プールには入りません。グループ化されていない counter、window、 correlation はキーで分割できないので、1 つのワーカーを共有します。
最後の一文が、興味深い結果の出どころです。
そしてコアを増やした
mixed ワークロード — value match と counter とウィンドウの組み合わせ、つまり実際のルールパックの形 — はスレッド 1 から 2 で 18% 改善し、4 で 23%、8 で 43% 失います。
原因は singleton プールです。window と correlation には分割するキーがないので、必然的に 1 ワーカーに 乗ります。スレッド 2 を超えると、追加のワーカーはその負荷を分担しているのではなく、動かせない直列区間の 周りでルーティング・チャネル・キャッシュコヒーレンスのコストを足しているだけです。
アムダールが予定どおり現れた、ということです。
労力はどこに使ったか
ホットパスに対する 8 回のパス。他人のエンジンについてなら私が読みたい部分なので、各回を正当化した数字 付きで書きます。
改善は賢さの追加ではなく作業の除去から来ました。ワーカープールごとのディープクローンを Arc の参照
カウント加算に置き換えるだけで 64%。ステートレス評価を呼び出し元スレッドへ移す — チャネルなし、
ワーカーなし — でさらに 32%。完全な OCSF セマンティック検証をスキーマのみのパースに替えて 49%。
この 3 番目はレビュアーに明示すべきもので、純粋な最適化ではなく能力のトレードだからです。Secrust は
基本フィールドをパースし、OCSF のセマンティクスは検査しません。これは隠さず文書化してあります。
6・7・8 回目は横ばいかわずかにマイナスですが、残しました。serde の flatten バッファリングを避ける
カスタム Deserialize、ゼロアロケーションの match-in-place フィルタ、Cow によるフィールド値の借用は
いずれもアロケーションの churn を除去する構造的な改善です。8 回目はマルチフィルタの AND 論理と Sigma の
大文字小文字非依存マッチングを加えたもので、スループットを少し払った正しさの修正です。
そのあと残り時間の内訳を測りました。この campaign で最も地味で、最も有用な作業です。
JSON パースはステートレスで 1 イベントあたりコストの 75%、ステートフルで 42% を占めます。つまり次の 本当の勝ち手は、より速い評価器ではありません。すでにパース済みのイベントを呼び出し元から受け取り、 パースを丸ごと省く API です。計算上、ステートレスのスループットは約 83.5 万から 500 万超になります。
これはまだ出荷していません。JSON を受け取るのではなくプログラム的にイベントを構築する呼び出し元だけが 得をする話に対して、公開 API の面を広げることになります。今シグネチャを推測するより、ワークロードを 添えて誰かが要求してきたときに追加したいと考えています。
作らないと決めたもの
リポジトリには Secrust と Flink を機能単位で比較した文書があり、Flink にあって Secrust にないものも すべて含まれています。書くのは気分の良い作業ではなく、プロジェクトで最も有用なページです。
重要な欠落を、私が直す順に並べます。
- スライディングウィンドウ — 設計済み、未実装。タンブリングウィンドウはエポック整列なので、 境界をまたぐバーストは 2 つのウィンドウとして数えられます。
- ウォーターマークと遅延データ処理 — これがないと、順序が乱れたイベントが誤ったウィンドウに入るか、 取りこぼされます。
- チェックポイント — プロセスが死ぬと、進行中の counter / window 状態は失われます。配信は at-least-once ではなく at-most-once です。
- ルールのホットリロード — ルールはエンジン構築時に読み込まれます。検知チームがルールを投入する ために再起動すべきではないのに、今はそうなっています。
どれも現在の設計の実際の限界であり、まとめて仮説の境界線を描いています。だから流行らないことをはっきり 書きます。流量が本当に大型マシン 1 台を超えるなら、あるいは相関状態が自分の制御できない再起動を またいで生き残る必要があるなら、Flink を動かしてください。 この一文はリポジトリにも書いてあります。 自分の撤退条件を言えないプロジェクトは、それを本番で探せと言っているのと同じです。
書き留めた鋭い縁
リポジトリには gotchas.md があり、初日に人を驚かせる挙動を列挙しています。各項目は原因となるコードに
リンクしており、このファイルが存在する理由には説明する価値のある判断が一つあります。
最悪のもの。timeframe を持たない Sigma ルールは、単一の AND 条件ではなくフィールドごとに独立した
value match ルールへ翻訳されます。つまりこれは —
detection:
selection:
class_uid: 4003
query.hostname|endswith: '.xyz'
condition: selection
— class_uid = 4003 または hostname が .xyz で終わる、になり、それぞれが単独で発報します。無害な
名前解決でも class が一致したのでルールが発火します。この YAML を書く人が意図するものではありません。
正しい修正はステートレス経路でフィールドを AND することです。それは既存の全ルールに影響する翻訳器の変更
なので、今のところは挙動を文書化し、回避策(timeframe を付けてフィルタが AND される counter 経路へ
回す)を示し、good first issue として登録しています。
同じファイルからもう 2 つ。作者しか知らない類のものだからです。count() > 5 は 6 件目ではなく 5 件目で
発火します。条件パーサーが数値だけを読み、演算子を無視するからです。そして temporal 相関から参照される
ルールは、ステートフルかつグループ化されていない必要があります。value match のアラートはインラインで
発報されるので相関器に届かず、グループ化された counter はハッシュ分割で相関器から離れてしまいます。
アラートはどこへ行くのか
Secrust は出す側で、受ける側が必要です。
Secrust が出すアラートは OCSF 形なので、Watari は 2 者間にアダプタなしで取り込みます。この 組み合わせは偶然ではありません。両プロジェクトがソースごとのパースではなくエッジでの単一スキーマに 標準化している理由そのものです。
統合面は 3 つあります。Rust API、C FFI の共有ライブラリ、そしてアラートのストリーミング購読を持つ gRPC サーバー。FFI があるのは、JVM 的なブリッジなしに Python・Go・C からエンジンを組み込めるように するためです。
使うべきでない場合
Rust 1.70 以降、MIT、そして正直に言えば alpha 寄りです。v1.0.0 なのは「これに対して開発できる程度に API が安定している」という意味で、「1 年の本番運用を生き延びた」という意味ではありません。
約 10 MB のメモリで、10 ミリ秒未満で起動し、何かに組み込めるサブミリ秒の相関が欲しいなら使ってください。 exactly-once セマンティクス、クラッシュリカバリ、分散スケールが必要なら使わないでください。
最も欲しい貢献は、単一イベントのマッチングではなく相関を働かせる検知ルールと、実運用の流量に当てて 倒れたという報告です。後者はスター 1,000 個より価値があり、私はそのどちらもほとんど持っていません。