/var/www/yatta47.log

/var/www/yatta47.log

やったのログ置場です。スクラップみたいな短編が多いかと。

CrowdStrike Falcon の Fargate 対応 — タスク定義パッチの仕組みと監視の落とし穴

ECS Fargate に CrowdStrike Falcon を入れるとき、「サイドカー方式で対応」と説明されることが多い。でもこの「サイドカー」という言葉が結構誤解を生む。実態を掘り下げてみたら、サイドカーとして動いているわけではなかった。

タスク定義パッチとは何をしているのか

CrowdStrike が提供するパッチユーティリティ(falconutil)は、既存のタスク定義 JSON を入力として受け取り、以下を変更した新しいタスク定義を出力する。

  1. CrowdStrike init コンテナを追加
  2. 各アプリコンテナのエントリポイントを書き換え
  3. SYS_PTRACE capability を各コンテナに付与
  4. 環境変数(CID 等)を追加
  5. 共有ボリュームの定義を追加

パッチ前後のタスク定義を見るとわかりやすい。

パッチ前:

{
  "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 での検知を組み合わせる必要がある。

参考