CrowdSecは、アクセスログから攻撃パターンを検知し、Remediation Component、いわゆるbouncerを通じて通信を遮断するセキュリティ基盤です。
導入記事を読むと、ログを読み込ませてbouncerを接続すれば完成したように見えます。
しかし、実際に運用して分かったのは、CrowdSecは入れてからが始まりだったということでした。
ログが正しく読まれ続けているか。正規の操作を攻撃と判定していないか。誤ってbanされたときにどう戻すか。そして、CrowdSec自身が止まったとき、公開サービスがどう動くか。
先に白状すると、私の環境で最も手がかかった相手は、外部のbotではなく「自分」でした。
この記事では導入手順ではなく、個人k3sクラスタでCrowdSecを使い続けるために必要だった運用を整理します。
先に結論
CrowdSecを導入した後に必要だった運用は、次の4つです。
- ログが読み込まれ続けているか確認する
- alertsとdecisionsを確認し、誤banを解除できるようにする
- 正常な操作を遮断しないよう、検知感度を調整する
- LAPIやbouncerが故障したときの動作を把握する
特に重要だったのは、4番目です。
当時使っていたTraefik向けbouncerのliveモードでは、キャッシュに結果がないIPについて、リクエスト時にLAPIへ問い合わせていました。
この状態でLAPIへの問い合わせに失敗すると、エラーを「許可」とせず、遮断として処理します。そのMiddlewareを複数の公開Routerに共通適用していたため、LAPI障害が複数ドメインの403につながりました。
つまり、私の構成では次の状態でした。
CrowdSec LAPIが停止
↓
bouncerが判定できない
↓
安全側に倒して拒否
↓
共通Middlewareを使うサービスが403
これはCrowdSec全体の一律の仕様ではありません。
使用するRemediation Component、バージョン、モード、キャッシュ、障害時設定によって挙動は異なります。導入時には「正常時に遮断できるか」だけでなく、判定サーバーへ到達できないときに許可するのか拒否するのかを確認する必要があります。
CrowdSecの3つの役割
障害を切り分けるには、CrowdSecの役割を分けて考える必要があります。
| 役割 | 仕事 | 私の構成での使い方 |
|---|---|---|
| Security Engine | ログを解析し、シナリオに一致する行動を検知する | Traefikのアクセスログを解析 |
| LAPI | alertsやdecisionsを保存し、bouncerへ提供する | クラスタ内の判定サーバー |
| Remediation Component | decisionに基づいて通信を遮断する | TraefikのMiddlewareとして適用 |
Security Engineが攻撃を検知しても、それだけでは通信は止まりません。
検知結果をLAPIがdecisionとして管理し、そのdecisionをbouncerが実際の403などへ変換します。
逆に言えば、次のように部分的に壊れることがあります。
- ログ取得が止まり、新しい攻撃を検知できない
- 検知はできているが、LAPIへalertを登録できない
- LAPIにdecisionはあるが、bouncerが取得できない
- bouncerは動いているが、送信元IPを正しく認識していない
「CrowdSecが動いているか」という一問だけでは足りません。
ログを読めているかは定期的に確認する
私の環境では、TraefikのアクセスログをJSON形式で出力し、Security Engineへ読み込ませています。
この配管で怖いのは、壊れても利用者から見たエラーが出ないことです。
ログの保存先、形式、権限、ローテーション方法などが変わると、CrowdSecが新しいログを読めなくなる場合があります。それでもWebサイト自体は通常どおり表示されます。
つまり、守れていないのに正常に見える状態になります。
現在のCrowdSecでは、Security Engine関連のメトリクスを次のように確認できます。
cscli metrics show engine
ここで主に見るのは、acquisitionとparserです。
- 読み込んだ行数が増えているか
- 対象のログが認識されているか
- parserで処理された行があるか
- scenarioへ渡されたイベントがあるか
CrowdSecの公式ドキュメントでも、acquisition metricsの読み込み行数がゼロならデータソースが停止または誤設定、解析行数がゼロならparserが機能していない可能性があると説明されています。
攻撃を検知した件数より先に、そもそもログを読めているかを確認します。
事件:自分をbanした
CrowdSecを導入してしばらくした頃、自分が管理しているWebサービスへ突然アクセスできなくなりました。
返ってくるのは403です。
アプリや認証基盤を調べる前にCrowdSecのdecisionを確認すると、自分の送信元IPが載っていました。
cscli decisions list
原因は、短時間に行った連続的な管理操作が、HTTPクローリングや探索行動に近いパターンとして検知されたことでした。
攻撃検知として完全に間違っていたわけではありません。
一般公開されたWebサイトでは、短時間に多数のURLへアクセスする行動はbotや探索行動である可能性があります。しかし、個人サイトでは、サイト全体を最も頻繁に操作する正規ユーザーも自分です。
その結果、最も熱心な正規ユーザーが、最も攻撃者らしく見えるという逆転が起きました。
誤banを解除する
IPに対するdecisionは、次のコマンドで削除できます。
cscli decisions delete --ip <対象のIP>
現在のcscliでは、--ipまたは-iを使って対象IPのdecisionを削除できます。
ただし、「コマンドが成功したら即座に403が消える」とは限りません。
bouncerが判定結果をキャッシュする構成では、LAPIからdecisionを削除しても、bouncer側のキャッシュが期限切れになるまで以前の判定が残る可能性があります。
解除後も403が続く場合は、次を確認します。
cscli decisions listから対象IPが消えたか- bouncerがlive、stream、noneのどのモードか
- bouncerのキャッシュ設定と有効期間
- Traefikまたはbouncerのログ
- CrowdSec以外のMiddlewareが403を返していないか
「decisionを削除すること」と「すべての遮断地点から状態が消えること」は、必ずしも同時ではありません。
恒久対応は感度を下げることではない
誤検知が起きると、対象シナリオを無効にしたくなります。
しかし、シナリオ全体を止めれば、本来検知したい攻撃も見えなくなります。
私が行ったのは、シナリオを削除することではなく、正常な管理操作では簡単にoverflowしないよう、個人サイトの利用実態に合わせて調整することでした。
CrowdSecのleaky bucket型シナリオでは、主に次の値が検知感度に関係します。
capacityleakspeed- イベントをまとめる
groupby - 重複イベントを除外する
distinct - overflow後の
blackhole
capacityはbucketが保持できるイベント数、leakspeedはイベントがbucketから減っていく間隔です。値を変更すると、誤検知と検知漏れのバランスが変わります。
記事では実際の値は公開しません。
重要なのは数値そのものではなく、次の順番です。
- どのシナリオが誤検知したか確認する
- alertの内容と実際の操作を照合する
- どの条件が正常操作と衝突したか調べる
- 必要最小限の調整を行う
- 調整後も本来の攻撃パターンを検知できるか確認する
単に閾値を大きくするだけでは、誤検知と一緒に検知能力も失います。
閾値調整で踏んだ3つの罠
罠1:ファイル名だけ合わせても調整できたとは限らない
CrowdSecのシナリオには、YAML内にname:があります。
一方、ファイル名はシナリオを配置するための名前です。
ローカルシナリオを配置するときに重要なのは、コピーしたファイル名だけを見て「公式シナリオを置き換えた」と判断しないことでした。
デプロイ後には、次のコマンドで実際の状態を確認します。
cscli scenarios list
確認するのは次の点です。
- 意図した
nameのシナリオが読み込まれているか - 同じ目的のシナリオが重複していないか
localまたはtaintedとして認識されているか- 調整前のシナリオが残っていないか
CrowdSecでは、Hub由来のファイルを変更するとtainted、自分で作成したものはlocalとして扱われます。これらは通常のHub更新では自動的に上書きされません。
また、変更したシナリオによるローカル検知は動きますが、公式Hubの未変更シナリオと同じ形ではコミュニティへのsignalとして扱われません。
調整には、更新追従やコミュニティ共有とのトレードオフがあります。
罠2:DISABLE_SCENARIOSへ何でも指定できるわけではない
Helm環境では、環境変数を使ってシナリオを無効化できる場合があります。
ただし、指定する名前はCrowdSecがHubアイテムとして認識できるシナリオ名である必要があります。
私はコレクション名、内部で参照される名前、実際のシナリオ名を混同し、Agentの初期化を失敗させました。
無効化する前に、少なくとも次を確認します。
cscli scenarios list
cscli collections list
確認せずに名前を推測して指定しないことが大切です。
また、閾値を調整したいだけなら、シナリオ全体を無効化するより、ローカルシナリオとして意図した条件を管理する方が分かりやすい場合があります。
罠3:アンダースコアが問題だったのはvolume名
調整したYAMLをConfigMapからマウントしようとした際、DeploymentがKubernetesのバリデーションで拒否されました。
最初はCrowdSecの設定ファイル名が原因だと思いました。
実際に問題だったのは、私のマニフェスト生成方法がファイル名をKubernetesのvolume名にも流用しており、生成されたvolume名にアンダースコアが入っていたことでした。
KubernetesのDNS-1123形式の名前には、英小文字、数字、ハイフンなどの制約があります。アンダースコアは使用できません。
重要なのは、マウント先のファイル名とKubernetesオブジェクト内のvolume名を区別することです。
「ConfigMap内のファイル名には必ずアンダースコアを使えない」という意味ではありません。
私の構成では、名前の生成規則を見直し、Kubernetes側の識別名をハイフン形式へ変更して解決しました。
「CrowdSecを止める」は復旧操作ではなかった
運用上、最も大きかった障害がLAPIへの接続不能でした。
当時の構成では、Traefik向けbouncerをliveモードで使い、複数の公開Routerへ共通適用していました。
liveモードでは、キャッシュに判定がないIPについてLAPIへ問い合わせます。確認したプラグインの実装では、この問い合わせがエラーになると、許可ではなくban相当の値を返します。
そのため、次の操作は復旧になりませんでした。
LAPIの調子が悪い
↓
とりあえずCrowdSecを停止する
↓
bouncerの問い合わせ先が完全になくなる
↓
403が続く
復旧方法は、次のどちらかです。
- LAPIとその依存先を復旧し、bouncerが再び判定できるようにする
- Traefik側でbouncer Middlewareの適用を安全に解除する
単にCrowdSecのPodを止めるだけでは、Traefik側のMiddleware参照は残ります。
この経験から、障害時の手順を次のように整理しました。
- bouncerのログでLAPI接続エラーか確認する
- LAPIのPod、Service、Endpointを確認する
- LAPIのデータベースやSecretなどの依存先を確認する
- LAPIを復旧する
- 復旧できない場合は、変更履歴を残しながらMiddlewareを一時解除する
- 復旧後に遮断機能を戻し、decision取得を確認する
セキュリティ機能を外す操作は、緊急時でもGitや作業記録に残します。
streamモードなら必ず止まらない、ではない
streamモードでは、bouncerがLAPIからdecisionの一覧を定期取得し、リクエストごとの判定をローカルキャッシュで行います。
そのため、毎リクエストLAPIへ問い合わせる方式より、LAPIへの依存と処理負荷を減らせます。確認したTraefik向けプラグインでも、streamモードが性能面から推奨されています。
ただし、streamモードへ変更するだけで、自動的に可用性優先になるわけではありません。
同プラグインには、stream更新に何回失敗したら通信を遮断するかを制御する設定があります。設定によっては、一定回数LAPIへ接続できなかった後、通信を止めます。逆に、接続できなくても遮断しない設定にすると、古いban表のまま通信を許可し続けることになります。
選択肢は次の2つです。
- fail-closed:判定基盤が壊れたら通信を止める
- fail-open:判定基盤が壊れてもサービス提供を続ける
どちらが正しいかは、サービスの性質によって変わります。
公開ブログと管理画面でも、同じ選択が正しいとは限りません。
大切なのは、デフォルト値に任せるのではなく、障害時の動作を意図して選ぶことです。
LAPIのデータベースは「学習結果」ではない
LAPIのデータベースには、alertsやdecisionsなどの状態が保存されます。
このデータベースを初期化すると、ローカルに保存されていた履歴や有効中のdecisionは失われます。CrowdSecのHelmドキュメントでも、LAPIには永続データベースを利用することが推奨されています。
ただし、これを「CrowdSecが学習した知識がすべて消える」と表現するのは正確ではありません。
失われるのは、主に次のデータです。
- ローカルで生成されたalerts
- ローカルで生成されたdecisions
- bouncerやmachineの登録情報
- 調査に使える過去の状態
一方、parserやscenarioの定義は設定ファイル側に残ります。Security Engineは新しいログを読み、再び検知を始められます。
LAPIのデータベースをバックアップ対象にするかは、次の観点で判断します。
- 過去のalertを調査に使うか
- 長期間の手動decisionを保持しているか
- bouncerやmachineを容易に再登録できるか
- 復旧手順を自動化しているか
私の場合、データそのもの以上に、初期化後に何を再登録する必要があるかを復旧手順へ残すことが重要でした。
現在の点検項目
現在は、次の項目を定期的に確認しています。
ログ取得
cscli metrics show engine
- 対象ログの読み込み行数が増えているか
- parserがイベントを処理しているか
- 想定外にunparsedが増えていないか
有効中の遮断
cscli decisions list
- どのIPが遮断されているか
- ローカル検知か、外部リスト由来か
- 不自然に大量のdecisionがないか
検知履歴
cscli alerts list
- 特定のシナリオだけが過剰に発火していないか
- 正規利用者を検知していないか
- 調整後に本来の攻撃が検知されているか
bouncerの状態
cscli bouncers list
- 想定したbouncerが登録されているか
- LAPIへのアクセスが継続しているか
- 古いbouncer登録が残っていないか
シナリオの状態
cscli scenarios list
localやtaintedのシナリオを把握しているか- 公式シナリオの更新から取り残されていないか
- 調整したシナリオと元のシナリオが重複していないか
コマンドを実行すること自体が目的ではありません。
「正常な状態では何が表示されるか」を把握し、前回との差分を見ることが重要です。
公開時に出さない情報
CrowdSecの運用記事では、設定例を見せすぎないことにも注意しています。
この記事では、次の情報を意図的に公開していません。
- bouncerのAPIキー
- 実際の送信元IP
- allowlistのアドレス範囲
- 調整後の閾値
- ban期間
- 内部のService名やURL
- 現在の障害時設定値
- 誤検知を再現する具体的な操作回数や間隔
一方、次の情報は公開しても大きな問題はないと判断しました。
- Security Engine、LAPI、bouncerの役割
- 誤検知が起きた事実
- 閾値を緩和した方向性
- 一般化した障害の切り分け方
- プレースホルダーを使った
cscliコマンド
運用の教訓は共有しつつ、現在の防御設定を再現できる情報は出さない方針です。
向いている人・向かない人
CrowdSecが向いているのは、すでに複数のサービスを公開し、アクセスログに探索や攻撃の痕跡が見えている人です。
共通のログ解析と遮断基盤を作ることで、サービスごとに防御ルールを実装する負担を減らせます。
一方、次のような人には向かない可能性があります。
- 入れれば自動的に守り続けてくれると考えている
- alertsやdecisionsを定期的に確認できない
- 誤検知時の解除方法を用意できない
- LAPI障害時の挙動を確認するつもりがない
- 公開サービスが少なく、アプリ側のレート制限で十分である
CrowdSecは運用負荷をゼロにする製品ではありません。
個別の攻撃ルールを一から作る負担を減らす代わりに、検知結果を観察し、自分の環境に合わせる仕事が残ります。
まとめ
CrowdSecを導入した後に必要だったのは、攻撃者との派手な攻防ではありませんでした。
実際に必要だったのは、次の地味な運用です。
ログを読めているか確認する
↓
alertsとdecisionsを見る
↓
誤検知を正常操作と照合する
↓
必要最小限の調整をする
↓
LAPI障害時の挙動を確認する
私の環境では、正規の管理操作が攻撃として検知され、自分自身がbanされました。
さらに、共通Middlewareとして使っていたbouncerがLAPIへ到達できなくなり、複数の公開サービスが一斉に403になりました。
この二つの経験から得た教訓は同じです。
セキュリティ機能は、正常時に攻撃を止められるだけでは足りない。誤って止めたときと、それ自身が壊れたときの戻し方まで含めて設計する。
CrowdSecで最も重要だった設定は、特定の閾値ではありませんでした。
誤検知を見つけられること、解除できること、そしてfail-openとfail-closedを自分で選んでいることでした。