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_procs や container はあらかじめ定義された 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 DEBUGFalco 設定で 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 container | privileged コンテナの実行 |
| 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-app、container_id=3f2a1b - 何のプロセスか:
shell=bash、cmdline=bash -iで対話型シェルであることを確認 - 誰が実行したか:
user=root - 親プロセス:
parent=runc
試験では「Falco ログでシェルが立ち上がった Pod の名前を見つけてファイルに書け」のような作業が出ます。Falco ログは通常 /var/log/syslog や journalctl -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.name | Pod 名 (Kubernetes メタ) |
evt.type | syscall の種類 |
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 ps や kubectl 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.logjq の select で条件をフィルタし、オブジェクト表記で欲しいフィールドだけを取り出すパターンを 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 分析。
jqのselectで特定のユーザー・リソース・動詞の条件をフィルタして答えを見つける
最もよくある減点は 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 まとまり。マウントの抜けが最もよくあるミス
- ログ分析は
jqのselectパターン 1 つでほとんど解決
次へ — Container immutability #
ランタイムで異常な行動を検知する方法をつかみました。しかし検知よりもっと根本的な対応は そもそもコンテナが変更されないようにする ことです。
#18 Container immutability、forensics では、readOnlyRootFilesystem でコンテナのファイルシステムを読み取り専用に固める方法、immutable コンテナのためのセキュリティコンテキスト設定、そして侵害が起きた後に証拠を収集して分析する forensics の基礎までを扱い、ランタイムドメインを締めくくります。