Certified Kubernetes Security Specialist (CKS) #17 Falco 行動分析、audit logs (Runtime)

#16 Admission control: OPA/Gatekeeper、Kyverno では、危険なマニフェストがクラスターに入る前に止める事前統制を扱いました。しかしすべての攻撃を入口で止めることはできません。正常にデプロイされた Pod の中で攻撃者がシェルを立ち上げたり、機密ファイルを読んだり、権限を上げたりする行為は、admission の段階をすでに過ぎた後に起きます。この記事は すでに動いているワークロードがランタイムで行う異常な行動を検知する 最後のドメイン Monitoring・Logging・Runtime Security を扱います。

ランタイムセキュリティの 2 つの軸ははっきりしています。ノードのカーネルで起きる syscall を監視する Falco、そして誰が API サーバーにどんなリクエストを送ったかを記録する audit log です。前者がコンテナ内部の行動を見るとすれば、後者はクラスターの control plane に入ってきたリクエストを見ると理解すればよいです。どちらも試験の常連なので、ルールとポリシーを自分で書き、出力を読む感覚をつかみます。

ランタイム脅威検知とは #

ここまでのドメインはほとんどが 事前統制 でした。NetworkPolicy で通信を止め、PSA で危険な Pod を拒否し、admission webhook でマニフェストを検査しました。これらの統制は攻撃が始まる前に動きます。しかし次のような状況を考えてみます。

  • 正常なイメージでデプロイされたコンテナが未知の脆弱性で乗っ取られた
  • 攻撃者がそのコンテナの中で /bin/bash を立ち上げて対話型シェルを得た
  • コンテナの中で /etc/shadow を読んだりホストディレクトリを探索したりする

これらの行為はマニフェストのレベルでは見えません。入口の統制をすべて通過した正常な Pod の中で起きるからです。ランタイム検知はこのように すでに動いているワークロードの実際の行動 を観察して異常を捉えます。止めるのではなく 見て知らせる ことが一次的な目標である点が事前統制と違います。

Falco: syscall ベースのルールエンジン #

Falco は CNCF のランタイムセキュリティツールで、Linux カーネルの system call (syscall) と Kubernetes audit イベントをリアルタイムで受け取り、あらかじめ定義したルールと突き合わせて違反をアラートとして送り出します。コンテナがシェルを立ち上げる瞬間、機密ファイルを開く瞬間、権限を上げる瞬間を syscall の流れから捉えます。

Falco が syscall を収集する方式は 2 つあります。カーネルモジュールや eBPF probe で直接カーネルからイベントを受け取ります。どちらにせよ結果的に同じルールエンジンが評価するので、試験では収集ドライバーよりも ルールを読み書きする能力 が重要です。

ルール構造: rule、condition、output、priority #

Falco ルールは YAML 1 項目で定義されます。核心となるフィールドは次のとおりです。

フィールド役割
ruleルール名。アラートに表示される
descルールの説明
conditionどのイベントを違反と見るかを決める式
outputアラート 1 行に何を入れるかを定義するテンプレート
priority深刻度 (EMERGENCY〜DEBUG)
tags分類タグ

最も基本となるシェル実行検知ルールを見ます。

- rule: Terminal shell in container
  desc: A shell was used as the entrypoint/exec target in a container
  condition: >
    spawned_process and container
    and shell_procs and proc.tty != 0
    and container_entrypoint
  output: >
    A shell was spawned in a container
    (user=%user.name container_id=%container.id
     container_name=%container.name shell=%proc.name
     parent=%proc.pname cmdline=%proc.cmdline)
  priority: NOTICE
  tags: [container, shell, mitre_execution]

condition はブール式です。spawned_process はプロセスが新しく立ち上がったイベント、container はそのイベントがコンテナの中で起きたという条件です。複数の条件を and で結んで、「コンテナの中でシェルプロセスが立ち上がった」というパターンを捉えます。

macro と list でルールを読みやすく #

上のルールに出てきた shell_procscontainer はあらかじめ定義された macro です。よく使う条件の断片に名前を付けて再利用します。list は値の束に名前を付けます。

- list: shell_binaries
  items: [bash, sh, zsh, ksh, csh, ash, dash]

- macro: shell_procs
  condition: proc.name in (shell_binaries)

list でシェルバイナリ名の束を定義し、macro で「プロセス名がその束に入っている」という条件に shell_procs という名前を付けます。こうするとルールの condition が短く読みやすくなります。試験で既存のルールを修正するとき、list に項目を 1 つ追加する方式で解く場合が多いです。

priority: 深刻度の等級 #

priority はアラートの深刻度を表し、上から下へ次の順序です。

EMERGENCY  ALERT  CRITICAL  ERROR  WARNING  NOTICE  INFORMATIONAL  DEBUG

Falco 設定で priority の閾値を置いて、一定の等級以上だけを出力するようにフィルタできます。試験で「WARNING 以上だけ見えるようにせよ」のような調整が出ることがあるので、等級の順序を覚えておくとよいです。

デフォルトルール: シェル実行、機密ファイルアクセス、権限昇格 #

Falco をインストールすると /etc/falco/falco_rules.yaml に検証済みのデフォルトルールが入っています。代表的な検知項目は次のとおりです。

デフォルトルール捉える行為
Terminal shell in containerコンテナの中での対話型シェル実行
Read sensitive file untrusted/etc/shadow/etc/sudoers などの機密ファイル読み取り
Write below etc/etc 配下へのファイル書き込み
Launch privileged containerprivileged コンテナの実行
Change thread namespaceコンテナがホスト namespace へ脱出を試みる
Mkdir binary dirs/bin/usr/bin などのバイナリディレクトリ変更

このデフォルトルールだけでもよくある侵入行為を幅広く検知します。デフォルトルールのファイルは 直接修正しないのが原則 です。Falco のアップグレード時に上書きされるからです。

カスタムルール: falco_rules.local.yaml #

ルールを追加したり既存のルールを上書きしたりするときは /etc/falco/falco_rules.local.yaml に書きます。Falco はデフォルトルールを先に読み、その後 local ファイルを読むので、local ファイルが後から適用されて デフォルトルールを安全に補強または再定義します。

たとえば特定のディレクトリへの書き込みを検知するカスタムルールを追加します。

# /etc/falco/falco_rules.local.yaml
- rule: Write to app config dir
  desc: Detect any write attempt under /app/config
  condition: >
    open_write and container
    and fd.name startswith /app/config
  output: >
    Write under /app/config detected
    (user=%user.name file=%fd.name
     container=%container.name command=%proc.cmdline)
  priority: WARNING
  tags: [filesystem, custom]

open_write は書き込みモードでファイルを開くイベントを捉えるマクロで、fd.name は対象ファイルのパスです。startswith でパスの接頭辞を比較します。ルールを追加した後は Falco を再読み込みさせる必要があります。

# systemd サービスとして動いている場合は再起動
systemctl restart falco

# ルールの文法だけ先に検査
falco -V -r /etc/falco/falco_rules.local.yaml

出力の読み取り: どの Pod、プロセス、syscall #

Falco のアラート 1 行には output テンプレートに入れたフィールドが埋められて出てきます。実際の出力はこのような姿です。

14:32:07.991 Notice A shell was spawned in a container
(user=root container_id=3f2a1b container_name=nginx-app
 shell=bash parent=runc cmdline=bash -i)

この 1 行から読むべきものははっきりしています。

  • どのコンテナか: container_name=nginx-appcontainer_id=3f2a1b
  • 何のプロセスか: shell=bashcmdline=bash -i で対話型シェルであることを確認
  • 誰が実行したか: user=root
  • 親プロセス: parent=runc

試験では「Falco ログでシェルが立ち上がった Pod の名前を見つけてファイルに書け」のような作業が出ます。Falco ログは通常 /var/log/syslogjournalctl -u falco、または設定によっては別ファイルに出るので、出力の場所から確認します。

# systemd ジャーナルで Falco アラートを確認
journalctl -u falco --no-pager | grep "shell was spawned"

# シェルが立ち上がったコンテナ名だけを抽出する例
journalctl -u falco --no-pager \
  | grep "Terminal shell in container" \
  | grep -oP 'container_name=\K[^ )]+'

よく使うフィールド名を整理しておきます。

フィールド意味
proc.nameプロセス名
proc.cmdline実行コマンドライン
proc.pname親プロセス名
user.name実行ユーザー
fd.nameアクセスしたファイルのパス
container.nameコンテナ名
container.idコンテナ ID
k8s.pod.namePod 名 (Kubernetes メタ)
evt.typesyscall の種類

audit log: Kubernetes API 監査 #

Falco がノードのカーネルの行動を見るとすれば、audit log は Kubernetes API サーバーに入ってきたリクエスト を記録します。誰がどのリソースにどんな動作 (get/create/delete) を要求し、結果が何だったかが残ります。「誰がこの Secret を読んだか」「どの ServiceAccount が Pod を作ったか」のような問いの答えがここにあります。

audit policy: 何をどれだけ記録するか #

監査ログは audit policy が定義します。すべてのリクエストを記録するとログが急増するので、どのリクエストをどの水準で残すかをポリシーでフィルタします。ポリシーファイルは通常 /etc/kubernetes/audit-policy.yaml に置きます。

記録水準は 4 つの level があります。

level記録内容
None記録しない
Metadataリクエストのメタデータのみ (誰が、何を、いつ)。本文を除く
Requestメタデータ + リクエスト本文
RequestResponseメタデータ + リクエスト本文 + レスポンス本文

また、リクエストが処理される時点を表す stage があります。

stage時点
RequestReceivedリクエストを受け取った直後
ResponseStartedレスポンスを送り始めた時点 (主に watch)
ResponseCompleteレスポンスが終わった時点
Panic内部パニックが発生

audit policy 例 #

ポリシーは上から下へルールを評価し、最初に一致したルールの level が適用 されます。したがってルールの順序が重要です。次は試験でよく変形される形です。

# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
# すべての RequestReceived stage は記録を省略 (ノイズ削減)
omitStages:
  - "RequestReceived"
rules:
  # Secret と ConfigMap はメタデータのみ残す
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets", "configmaps"]

  # 特定の namespace の Pod 変更は本文まで
  - level: Request
    namespaces: ["prod"]
    resources:
      - group: ""
        resources: ["pods"]
    verbs: ["create", "update", "delete"]

  # 読み取り専用のシステムリクエストは記録しない
  - level: None
    users: ["system:kube-proxy"]
    verbs: ["watch", "get"]

  # それ以外のすべてのリクエストはメタデータのみ
  - level: Metadata

上のポリシーは Secret と ConfigMap はメタデータのみ、prod namespace の Pod 変更は本文まで、kube-proxy の読み取りは無視、残りはメタデータのみを残します。最後の catch-all ルール (level: Metadata) を抜かすと一致しないリクエストが記録されないので注意します。

apiserver フラグ設定 #

ポリシーファイルを作ったら、API サーバーがそのポリシーを使うようにフラグを有効にする必要があります。kubeadm クラスターでは /etc/kubernetes/manifests/kube-apiserver.yaml を直接編集します。

フラグ役割
--audit-policy-file適用する audit policy ファイルのパス
--audit-log-path監査ログを書くファイルのパス
--audit-log-maxageログファイルの保管日数
--audit-log-maxbackup保管するログファイルの個数
--audit-log-maxsizeログファイルのローテーションサイズ (MB)

kube-apiserver は static Pod なので、ポリシーファイルとログディレクトリを hostPath ボリュームとしてマウント してあげないとコンテナの中でアクセスできません。このマウントを抜かして apiserver が立ち上がらないミスが試験で最もよくあります。

# /etc/kubernetes/manifests/kube-apiserver.yaml (抜粋)
spec:
  containers:
    - name: kube-apiserver
      command:
        - kube-apiserver
        - --audit-policy-file=/etc/kubernetes/audit-policy.yaml
        - --audit-log-path=/var/log/kubernetes/audit/audit.log
        - --audit-log-maxage=7
        - --audit-log-maxbackup=2
        - --audit-log-maxsize=50
        # 既存のフラグ...
      volumeMounts:
        - name: audit-policy
          mountPath: /etc/kubernetes/audit-policy.yaml
          readOnly: true
        - name: audit-log
          mountPath: /var/log/kubernetes/audit/
          readOnly: false
  volumes:
    - name: audit-policy
      hostPath:
        path: /etc/kubernetes/audit-policy.yaml
        type: File
    - name: audit-log
      hostPath:
        path: /var/log/kubernetes/audit/
        type: DirectoryOrCreate

マニフェストを保存すると kubelet が apiserver Pod を自動で立ち上げ直します。立ち上がり直すのに時間がかかるので、crictl pskubectl get pods -n kube-system で apiserver が正常に起動したかを確認します。立ち上げられない場合は、ログパスのディレクトリ権限や hostPath マウントの抜けを先に疑います。

ログ分析 #

監査ログは 1 行に JSON 1 件が入る形式です。1 件を見ます。

{
  "kind": "Event",
  "level": "Metadata",
  "stage": "ResponseComplete",
  "requestURI": "/api/v1/namespaces/prod/secrets/db-cred",
  "verb": "get",
  "user": { "username": "dev-user" },
  "objectRef": {
    "resource": "secrets",
    "namespace": "prod",
    "name": "db-cred"
  },
  "responseStatus": { "code": 200 }
}

この 1 件から「dev-user が prod namespace の db-cred Secret を読み、成功した」を読み取ります。試験では jq で特定の条件をフィルタする作業が出ます。

# 特定の Secret にアクセスしたユーザーだけを抽出
jq 'select(.objectRef.resource=="secrets"
      and .objectRef.name=="db-cred")
      | .user.username' \
  /var/log/kubernetes/audit/audit.log

# delete 動作だけを時系列で見る
jq 'select(.verb=="delete")
      | {time:.requestReceivedTimestamp,
         user:.user.username,
         res:.objectRef.resource}' \
  /var/log/kubernetes/audit/audit.log

jqselect で条件をフィルタし、オブジェクト表記で欲しいフィールドだけを取り出すパターンを 1 つ手に馴染ませておけば、ほとんどの分析作業を素早く終えられます。

試験ポイント #

ランタイムドメインで実技としてよく出る作業を整理します。

  • Falco ルールで異常検知。既存のルールを読んでどんな行為を捉えるか把握したり、list に項目を追加する形で検知範囲を広げたりする作業
  • Falco 出力から情報抽出。ログからシェルが立ち上がったコンテナ・Pod 名や実行ユーザーを見つけて指定ファイルに書く。ログの場所 (journalctl -u falco または設定ファイルのパス) から確認
  • カスタムルール作成/etc/falco/falco_rules.local.yaml にルールを追加し、Falco を再読み込みさせて適用
  • audit policy 作成。要求されたリソース・動詞・namespace に合う level とルールの順序を正確に書く。最後の catch-all ルールを抜かさない
  • apiserver 監査の有効化--audit-policy-file--audit-log-path フラグの追加と hostPath ボリュームマウント を一緒に処理。apiserver が立ち上がり直すか必ず確認
  • audit log 分析jqselect で特定のユーザー・リソース・動詞の条件をフィルタして答えを見つける

最もよくある減点は 2 つです。audit policy をうまく書いておきながら apiserver マニフェストに hostPath マウントを抜かして apiserver が死ぬ場合、そして Falco ルールを追加した後に再読み込みされず適用されない場合です。ポリシーとルールは 反映と起動確認まで が 1 つの作業であることを忘れないようにします。

まとめ #

この記事で押さえたこと:

  • ランタイム脅威検知は事前統制が見逃した行動を見る。正常にデプロイされた Pod の中のシェル実行・機密ファイルアクセス・権限昇格をリアルタイムで捉える
  • Falco は syscall ベースのルールエンジンrule/condition/output/priority の構造、macro と list で条件を再利用、デフォルトルールには手を付けず falco_rules.local.yaml にカスタムを書く
  • Falco 出力はコンテナ・プロセス・ユーザー・syscall を 1 行に 入れる。ログの場所を先に見つけてフィールドを抽出
  • audit log は API サーバーのリクエストを記録。level (None/Metadata/Request/RequestResponse) と stage、ルールの順序が核心で、catch-all ルールを最後に置く
  • apiserver 監査の有効化はフラグ + hostPath マウント + 起動確認 が 1 まとまり。マウントの抜けが最もよくあるミス
  • ログ分析は jqselect パターン 1 つでほとんど解決

次へ — Container immutability #

ランタイムで異常な行動を検知する方法をつかみました。しかし検知よりもっと根本的な対応は そもそもコンテナが変更されないようにする ことです。

#18 Container immutability、forensics では、readOnlyRootFilesystem でコンテナのファイルシステムを読み取り専用に固める方法、immutable コンテナのためのセキュリティコンテキスト設定、そして侵害が起きた後に証拠を収集して分析する forensics の基礎までを扱い、ランタイムドメインを締めくくります。

X