ECS Fargate に CrowdStrike Falcon を入れるとき、「サイドカー方式で対応」と説明されることが多い。でもこの「サイドカー」という言葉が結構誤解を生む。実態を掘り下げてみたら、サイドカーとして動いているわけではなかった。
タスク定義パッチとは何をしているのか
CrowdStrike が提供するパッチユーティリティ(falconutil)は、既存のタスク定義 JSON を入力として受け取り、以下を変更した新しいタスク定義を出力する。
- CrowdStrike init コンテナを追加
- 各アプリコンテナのエントリポイントを書き換え
SYS_PTRACEcapability を各コンテナに付与- 環境変数(CID 等)を追加
- 共有ボリュームの定義を追加
パッチ前後のタスク定義を見るとわかりやすい。
パッチ前:
{ "containerDefinitions": [ { "name": "app", "image": "my-app:latest", "entryPoint": ["/app/start.sh"] } ] }
パッチ後:
{ "containerDefinitions": [ { "name": "crowdstrike-init", "image": "crowdstrike/falcon-sensor:latest", "essential": false, "mountPoints": [{"sourceVolume": "falcon-vol", "containerPath": "/falcon"}], "command": ["cp", "-r", "/opt/CrowdStrike/.", "/falcon/"] }, { "name": "app", "image": "my-app:latest", "entryPoint": ["/falcon/falcon-sensor", "--", "/app/start.sh"], "linuxParameters": {"capabilities": {"add": ["SYS_PTRACE"]}}, "mountPoints": [{"sourceVolume": "falcon-vol", "containerPath": "/falcon"}], "dependsOn": [{"containerName": "crowdstrike-init", "condition": "SUCCESS"}] } ], "volumes": [{"name": "falcon-vol"}] }
エントリポイントが /app/start.sh から /falcon/falcon-sensor -- /app/start.sh に変わっている。センサーが先に起動して、その後に元のアプリが起動する。
アーキテクチャ — デプロイ時とランタイムで姿が変わる
ここが「サイドカー」という言葉で誤解しやすいところ。デプロイ時とランタイムで構成が違う。
デプロイ時: falconutil がタスク定義を書き換える
graph LR
subgraph CI/CD Pipeline
DEV["開発者"] --> |タスク定義JSON| FU["falconutil"]
FU --> |パッチ済みタスク定義| ECS["ECS<br/>RegisterTaskDefinition"]
end
FU -.-> |追加| INIT["CS Init Container定義"]
FU -.-> |書き換え| EP["各コンテナの<br/>entrypoint"]
FU -.-> |付与| PTRACE["SYS_PTRACE"]
FU -.-> |追加| VOL["共有ボリューム定義"]
style FU fill:#e3f2fd
style INIT fill:#fff3e0
style EP fill:#fff3e0
style PTRACE fill:#fff3e0
style VOL fill:#fff3e0
ランタイム: init container は消え、各コンテナが独立してセンサーを動かす
graph TB
subgraph ECS Task
INIT["CS Init Container<br/>(終了済み)"]
INIT --> |共有ボリュームに<br/>バイナリコピー| VOL[("共有ボリューム<br/>/falcon/")]
subgraph APP1["App Container 1"]
SENSOR1["falcon-sensor<br/>(PID 1)"]
REAL_APP1["元のアプリ<br/>(子プロセス)"]
SENSOR1 --> REAL_APP1
end
subgraph APP2["App Container 2"]
SENSOR2["falcon-sensor<br/>(PID 1)"]
REAL_APP2["元のアプリ<br/>(子プロセス)"]
SENSOR2 --> REAL_APP2
end
VOL --> |マウント| APP1
VOL --> |マウント| APP2
end
SENSOR1 --> |テレメトリ| CS["CrowdStrike Cloud"]
SENSOR2 --> |テレメトリ| CS
style INIT fill:#e0e0e0,stroke-dasharray: 5 5
style SENSOR1 fill:#e8f5e9
style SENSOR2 fill:#e8f5e9
style VOL fill:#fff3e0
ランタイムでは init コンテナは既に終了している。「サイドカーが横から監視している」わけではなく、各コンテナの中で falcon-sensor が PID 1 として起動し、元のアプリを子プロセスとして動かしている。
つまり「サイドカー」が指しているのはデプロイのメカニズム(init コンテナでバイナリを配布する部分)であって、ランタイムのアーキテクチャではない。
なぜこの構造になるのか
EC2 との比較で考えるとわかりやすい。
graph TB
subgraph EC2["EC2 の場合"]
HOST_SENSOR["Falcon Sensor<br/>(ホストOS上、カーネルレベル)"]
C1_EC2["Container 1"]
C2_EC2["Container 2"]
C3_EC2["Container 3"]
HOST_SENSOR -.->|全プロセスを監視| C1_EC2
HOST_SENSOR -.->|全プロセスを監視| C2_EC2
HOST_SENSOR -.->|全プロセスを監視| C3_EC2
end
subgraph Fargate["Fargate の場合"]
C1_FG["Container 1<br/>+ Falcon Sensor"]
C2_FG["Container 2<br/>+ Falcon Sensor"]
C3_FG["Container 3<br/>+ Falcon Sensor"]
end
style HOST_SENSOR fill:#e8f5e9
style C1_FG fill:#fff3e0
style C2_FG fill:#fff3e0
style C3_FG fill:#fff3e0
EC2 ではホスト OS にセンサーを1つ入れるだけで、カーネルレベルで全コンテナのプロセスを監視できる。Fargate ではホスト OS にアクセスできないので、CrowdStrike の現行実装では各コンテナの中にそれぞれセンサーを注入する方式を採用している。
なお、2023年8月に Fargate が pidMode: task(タスク内の PID namespace 共有)をサポートしたので、理論上はサイドカーから他コンテナのプロセスを監視することも可能になっている。ただし CrowdStrike はエントリポイント注入方式を選択しており、pidMode に依存しない設計になっている。
さらに Fargate のセンサーはカーネルモジュールを使えないので、ユーザー空間で SYS_PTRACE + /proc を使って監視する。カーネルレベルのイベント(eBPF / kprobe 等)が使えないため、ホストセンサーより検知できるイベント種別が限られる。
Falcon Discover の限界 — 「見えないものは見つけられない」
EC2 ではホストセンサーがカーネルレベルで全コンテナを観測できるので、CrowdStrike 側から「センサーが入っていないコンテナ」を検知できる。
Fargate ではこの観測点が存在しない。ホストセンサーがないので、CrowdStrike 側からはセンサーが注入されたコンテナしか見えない。パッチが適用されていないタスクは CrowdStrike の視界に入らないため、「入れるべきものに入っているかどうか」を CrowdStrike 単体で判断することができない。
EC2: ホストセンサー → 全コンテナが見える → 未管理コンテナを発見 ✓ Fargate: ホストセンサーなし → センサー入りコンテナだけ見える → 未管理コンテナは死角 ✗
つまり Falcon Discover は「見えている範囲の中で未管理を見つける」機能であって、「見えない場所を見つける」機能ではない。
監視漏れをカバーする方法
CrowdStrike 側から未パッチのタスクを検知できない以上、AWS 側の仕組みで補う必要がある。
デプロイ時: パイプラインで縛る
パッチユーティリティを CI/CD パイプラインに組み込み、パッチを通さないデプロイを禁止する。
flowchart LR
DEV["開発者"] --> PR["PR / マージ"]
PR --> CI["CI/CD"]
CI --> PATCH["falconutil<br/>タスク定義パッチ"]
PATCH --> CHECK{"パッチ適用済み?"}
CHECK -->|Yes| DEPLOY["ECS デプロイ"]
CHECK -->|No| BLOCK["デプロイ拒否"]
DIRECT["直接デプロイ<br/>(パイプライン迂回)"] -.->|禁止する必要あり| ECS["ECS"]
style BLOCK fill:#ffcdd2
style DIRECT fill:#ffcdd2
ただしこれはパイプラインを通るデプロイにしか効かない。AWS コンソールや CLI から直接タスク定義を登録されると、パッチなしのタスクが動いてしまう。IAM ポリシーで ecs:RegisterTaskDefinition を CI/CD ロールに限定するなどの対策が必要。
運用時: AWS Config + EventBridge で未パッチを検知
既存のタスク定義と新規登録をそれぞれ監視する。
- AWS Config カスタムルール: 全タスク定義を定期スキャンし、CrowdStrike init コンテナが含まれていないものを検知
- EventBridge:
RegisterTaskDefinitionイベントを監視し、新規登録されたタスク定義をリアルタイムで検査
この2つで「既存の漏れ」と「新規の漏れ」の両方をカバーできる。
補足: GuardDuty は性質が違う
GuardDuty Runtime Monitoring は Fargate に対応しており、GuardDuty がサイドカーを自動管理するため手動でのエージェント追加が不要。ただし CrowdStrike とは性質が異なる。
- CrowdStrike: 全プロセスを常時監視 → 悪意ある動きを即座にブロック(予防)
- GuardDuty: 異常パターンを検知 → 起きた後に気づく(検知)
GuardDuty があるから CrowdStrike は要らない、にはならない。逆に CrowdStrike が入っていれば GuardDuty は不要、でもない。予防と検知は別の層。
まとめ
CrowdStrike の Fargate 対応は「サイドカー方式」と呼ばれているけど、実態は「init コンテナでバイナリを配布 → 各コンテナのエントリポイントを書き換えてセンサーを注入」する方式。ランタイムではサイドカーは存在せず、各コンテナが独立してセンサーを動かしている。
この構造を理解しておくと、「サイドカーを1つ足せば全部監視できる」という誤解を避けられるし、Fargate で CrowdStrike 側から未パッチタスクを検知できない理由も腑に落ちる。監視漏れのカバーは CrowdStrike 単体ではできないので、CI/CD パイプラインの強制 + AWS Config/EventBridge での検知を組み合わせる必要がある。