スマートフォン・タブレットからインターネットサーバーオペレーション

APPW.jp
 
baseline contamination

自分の修正が、比較の土台を壊した
ベースラインという前提

変化検知が「404が107倍に増えました」と報告してきました。計算は正しい。実装にバグもない。 それでも、その数字には意味がありませんでした。 攻撃者が増えたのではなく、4日前に自分が設定を直したからです。 比較の土台が、いつのまにか2つの世界にまたがっていた話です。

変化検知 ベースライン ソフト404 監視

[ § 0 ] 導入

「404が107倍に増えました」

前回、zスコアが使えないと分かって判定を百分位と倍率に作り替えました。 今度はちゃんと検知します。画面を開くと、こう出ていました。

変化検知タブ
404 探索 ★急増
  同時刻帯の中央値 2 → 直近平均 215.3 (107.67 倍)

107倍。さすがに何かおかしい。攻撃が一晩で百倍になることは、まず起きません。 計算は正しいのに、意味がない——そういう数字でした。

[ § 1 ] 調査

まず、計算を疑った

直前にzスコアで失敗していたので、また手法の選択を間違えたのだと思いました。 百分位も倍率も、どこか実装を誤ったのだろうと。

ところが検算しても、すべて正しいのです。 同時刻帯の中央値は確かに2、直近6時間の平均は確かに215.3。割れば107.67倍。 コードにバグはありませんでした。

そこで、数字そのものを見直しました。中央値2という値は、本当にあり得るのか。

[ § 2 ] 発見

中央値が「2」だった理由

404が毎時2件。スキャナが毎日大量に来ている環境で、この数字はあり得ません。 /wp-login.php/.env も、放っておいても叩かれ続けます。

カレンダーを見て、気づきました。4日前に、自分でソフト404を直していたのです。

時期存在しないパスへの応答404として記録
修正前301 → 200(ソフト404)されない
修正後404される

前々回の記事で直したとおり、 それ以前は存在しないパスが 301 を経て 200 を返していました。 探索は来ていたのに、404として数えられていなかったのです。

つまり、中央値2は正確な値でした。 「修正前の記録」としては、まったく正しい。おかしいのは値ではなく、 その値を「いつもの状態」とみなしたことのほうでした。
[ § 3 ] 構造

ベースラインが、2つの世界にまたがっていた

ベースラインは直近の前の168時間(7日)を取っていました。 その7日間の中に、計測方法が変わった瞬間が含まれていたのです。

ベースライン168時間の中身
├─────────── 修正前 ───────────┤├─── 修正後 ───┤│ 直近6h │
   404が化けていた世界           正しく数える世界
   (記録は毎時 2件前後)        (記録は毎時 200件超)
                            ↑
                      ここで計測の意味が変わった

同じ「404」という名前がついていますが、前半と後半では中身が別物です。 前半は「ソフト404をすり抜けた分だけの404」、後半は「本来の404」。 違う定義の指標を、ひとつの母集団として混ぜていました

比較は成立しません。前提が壊れているのだから、どんな統計手法を使っても正しい答えは出ない。 zスコアの件は手法の選択ミスでしたが、今度はもっと手前の問題でした。

[ § 4 ] 挙動

窓を動かすと、数字が崩落する

では、ベースラインの長さを変えるとどうなるか。 変更点を「直近の48時間前」に置いた再現データで確かめました。

ベース長変更前を含む同時刻帯の中央値倍率判定
24h0h195.51.0倍変化なし
48h0h201.31.0倍変化なし
60h12h190.31.1倍変化なし
72h24h184.51.1倍変化なし
84h36h97.52.1倍増加
108h60h3.068.3倍★急増
168h120h3.068.3倍★急増

連続的に悪化しない

注目してほしいのは、倍率がなだらかに動かないことです。 1.0倍のまましばらく続き、ある一点で68倍へ跳ねます。 中央値が「後半の世界」から「前半の世界」へ、一気に切り替わるためです。

中央値は順位で決まります。前半のデータが半数を超えた瞬間、代表値は入れ替わる。 少しずつずれていくのではなく、ある日を境に別の数字になる

どちらの世界にも存在しない値

さらに厄介なのが、切り替わりの境目です。ベース長84時間のとき、中央値は 97.5 でした。

97.5 という値は、どこにも実在しません。 修正前は毎時2件前後、修正後は200件超。 97.5 はその谷間に落ちた数字で、どちらの世界も代表していません。 二峰分布の中央値は、しばしばこういう「誰でもない値」を返します。

汚染されていても、正常に見える領域がある

もうひとつ気づいたことがあります。変更点は48時間前なので、 ベース長が48時間を超えればその時点で汚染されています。 ところが60〜72時間の範囲では、倍率は1.1倍。数字は正常に見えます

「またいでいるのに、まだ壊れていない」状態です。 数字が正常だからといって、ベースラインが健全だとは限らない—— これは画面を見ているだけでは絶対に分かりません。

[ § 5 ] 定義

ベースラインとは、何か

今回の件で、定義を書き直すことになりました。

ベースラインは「過去のデータ」ではない。 「同じ条件で計測された過去のデータ」である。

条件が変われば、いくらデータが残っていても比較には使えません。 そして条件が変わる理由は、いくつかあります。

壊れ方データから分かるか
収集していない期間cron を止めていた、導入前分かる(記録が無い)
計測の意味が変わったソフト404の修正分からない
対象が変わったサイト追加、遮断ルール変更分からない

1つ目は自動で防げます。記録が存在しないので、集計側で「収集開始より前は母集団に含めない」と決めれば済む。 実際にその処理は入れてありました。

問題は2つ目と3つ目です。データはちゃんと存在している。 欠損もない。数字も正しい。それでも比較には使えない。 データの中を見ても、意味が変わったことは分からないのです。

[ § 6 ] 対処

いま、できること

ベースラインを短くする

いちばん確実で、すぐできる対処です。変更点より後だけを見る。 API にベースラインの長さを渡すパラメータを持たせてあったので、値を変えるだけで済みました。

窓を3日に切り詰める
# 既定は 168 時間(7日)
GET /log-api.php?tab=changes&recent=6&base=168

# 変更点をまたがないよう 72 時間に
GET /log-api.php?tab=changes&recent=6&base=72

短くすればサンプルは減り、判定は不安定になります。精度と正しさの取り引きです。 ただ、前提の壊れた長い窓より、前提の揃った短い窓のほうがましです

収集前をゼロと誤認しない

こちらは実装済みでした。集計に存在する最も古いバケットを調べ、それより前は母集団に含めない。 cron を回し始める前の期間を「トラフィックゼロだった」と誤読するのを防ぐ処理です。

ベースラインの開始を、実データの範囲に制限する
// 集計に存在する最古のバケットを取得
const earliest = min(bucket);

// 要求された開始位置より後ろなら、そちらを採用する
const baseStart = earliest > requestedStart ? earliest : requestedStart;

これで1つ目の壊れ方は防げます。ただし今回の件には効きません。 収集は動いていて、データも揃っていたからです。

残った部分は、人間が覚えておくしかありません。 設定を変えたことは、ログのどこにも書かれていない。 数字が跳ねたとき、まず自分の変更履歴を見る—— いまのところ、これがいちばん確実な手続きです。
[ § 7 ] 体感デモ

ベースラインの窓を、動かす

変更点を48時間前に置いた再現データです。 スライダーでベースラインの長さを変えると、報告される倍率がどう動くか見てください。

Interactive · baseline window
窓の長さと、報告される倍率
実運用の状況を模した再現データです。直近6時間は固定で、ベースラインの長さだけが変わります。
ベースラインの長さ 168h
404の記録数(1時間ごと)— 明るい帯が、いま使っているベースライン
404の記録数 設定を変更した時点 直近6時間

168時間から縮めていくと、しばらく 68倍 のまま動きません。 あるところで 2.1倍 を通り、さらに縮めると 1.0倍 に落ち着きます。 同じデータ、同じ計算です。変えたのは窓の長さだけ。

[ § 8 ] まとめ

改善するほど、比較できなくなる

この件には、監視という営みの根本的な緊張関係が現れていると思います。

システムを直すたびに、過去との比較が無効になる。 ソフト404を直したのは正しい判断でした。計測が正確になり、攻撃の実態が見えるようになった。 その代償として、修正前のデータが比較に使えなくなったのです。

皮肉なことに、ずっと壊れたままのシステムは、比較しやすい。 条件が変わらないからです。改善し続けるシステムほど、ベースラインは頻繁に無効になります。

この記事のまとめ

  • 「107倍」は計算として正しく、意味として無意味だった。バグではなく前提の問題。
  • ベースライン168時間の中に計測方法の変更点が入っていた。前半と後半は別の指標。
  • 倍率は連続的に悪化しない。中央値が入れ替わる一点で跳ねる。
  • 切り替わりの境目では、どちらの世界にも実在しない代表値が出る(二峰分布の谷)。
  • 汚染されていても数字が正常に見える領域がある。画面だけでは判断できない。
  • ベースラインとは「同じ条件で計測された過去のデータ」。条件が変われば使えない。
  • 収集前の誤認は自動で防げるが、意味の変化はデータからは分からない
  • 数字が跳ねたら、まず疑うのは攻撃者ではなく自分の変更履歴

関連記事

『自分の修正が、比較の土台を壊した — ベースラインという前提』を公開しました。