新卒インフラエンジニア向け「Kubernetes 実践教科書 ③ CKS セキュリティ・ハードニング編」(全19回)の第15回です。前回は SBOM と署名で成果物の出所を確かめ、Admission でクラスタの入口を絞り、最後にその Admission を撤去しました。今回から D6 Monitoring, Logging and Runtime Security(配点 20%) に入ります。D6 の 3 回のうち 1 回目です。
本回の主題は 「動き出した後を見る」です。第12〜14回で作った 4 つの関門をすべて通ったコンテナの中で、シェルが起動したり /etc/shadow が読まれたりしたとき、それを捉えるのが Falco です。捉える対象は syscall なので、アプリケーションの改修は要りません。 ルールを書けば、既存のコンテナをそのままにして検知の網を張れます。
そして本回も、道具が既定のままでは動かないところから始まります。 ただし第13回(Trivy の DB 配布元)・第14回(cosign v3 のフラグ)とは詰まり方が違います。本回は 2 段階で詰まります——①chart の配布元に届かない(helm repo add が Forbidden)②届く経路で入れたら、5 台中 4 台しか起動しない。しかも2 番目は helm install が成功した後に起きます。
- ラボ Kubernetes v1.36.3
- CKS 試験環境 v1.35
- Falco 0.44.1(Helm chart 9.1.0)
- falcoctl 0.13.0
- container plugin 0.7.1
- ルールセット
falco-rules:5 - Helm v4.1.4
- Cilium v1.19.6
- containerd v2.2.6
- AlmaLinux 10.2(kernel 6.12.0-211.22.1.el10_2.x86_64)
- ArgoCD v3.4 系
- 確認日 2026-08-02
Falco 本体は 0.44.1 で、Helm chart は 9.1.0 です。 数字がまったく揃っていないので、「chart のバージョン」と「Falco 本体のバージョン」を混同しないでください。 helm show chart の出力には version: 9.1.0 と appVersion: 0.44.1 が両方載ります。ピン留めするのは chart の側(--version 9.1.0)で、本体のバージョンは chart が決めます。
目次
- 第15回のスコープ・今ここマップ
- この回のゴール
- Falco は何を見ているか —— syscall とドライバ、「防ぐ層」と「気づく層」
- 配布元を選び直す —— helm repo add は塞がれているが OCI は通る
- やってみよう①:Falco を導入し、1 台だけ上がらない事象を切り分ける
- falcoctl はルールを ghcr.io から取り、cosign で検証している —— 第14回が道具の内側にある
- 既定ルールは 25 本しかない —— rule / macro / list と priority
- やってみよう②:kubectl exec が検知されないことを確認する
- カスタムルールはどこに置かれ、いつ反映されるか —— rules.d/ と 2 つの経路
- やってみよう③:カスタムルールを書き、sidecar が漏れることを見つける
- kubectl debug は PSA restricted の namespace で 3 段階に詰まる
- やってみよう④:誤検知を再現してから潰す
- 監視する側は何に守られているか —— Falco 自身が spc_t である
- 暗記必須コマンドと、ラボ固有の事情の切り分け
- まとめ
- 理解度チェック(○×形式・全 9 問)
- 次回予告
第15回のスコープ・今ここマップ
D6 は 5 つのコンピテンシーで構成されます。本回はその 1 つ目を担当します。
| 公式コンピテンシー(D6 Monitoring, Logging and Runtime Security・20%) | 担当 |
|---|---|
Perform behavioral analytics to detect malicious activities | 本回(第15回) |
Detect threats within physical infrastructure, apps, networks, data, users and workloads | 第17回 |
Investigate and identify phases of attack and bad actors within the environment | 第16回・第18回 |
Ensure immutability of containers at runtime | 第17回 |
Use audit logs to monitor access | 第16回 |
残る 4 つは 第16回(監査ログ・攻撃フェーズ)・第17回(6 面の脅威検知・不変性・アラート連携)・第18回(インシデント対応の実戦)で埋まります。本回で入れる Falco は、第17回と第18回が土台として使います。
本回の軸: 見えていないものは、見えていないと分からない
本回は 4 回、同じ形の失敗をします。 どれも「動いているのに何も出ない」あるいは「成功と表示されるのに終わっていない」という形で現れます。
| # | どこで | 何が起きるか | 誤読しやすい内容 |
|---|---|---|---|
| ① | 演習①(導入) | 5 台中 4 台しか Falco が上がりません | kubectl get pods -n falco を見ずに「入った」と判断すると、1 ノード上のコンテナだけ 1 件も監視されていない状態で先へ進みます |
| ② | 演習②(既定ルール) | kubectl exec -- sh -c 'id' が 1 件も検知されません | 「Falco が壊れている」と考えます。実際は proc.tty != 0 という条件どおりの正常動作です |
| ③ | 演習③(カスタムルール) | 同じ Pod の sidecar でシェルを起動しても検知されません | 「ルールが効いている」と判断します。効いているのは backend コンテナだけです |
| ④ | 演習④(書き戻し) | helm upgrade が失敗しているのに kubectl rollout status は成功と表示します | 「反映できた」と判断します。実際は ConfigMap の適用で止まっており、DaemonSet には触れていません |
本回の中心メッセージ: 検知の仕組みは、検知しなかったときに何も言いません。
「アラートが出ていない」には 2 つの意味があります——何も起きていないのと、見ていないのです。この 2 つを区別する手段を持たないまま検知基盤を運用すると、「静かだから安全だ」と読んでしまいます。 D6 の 3 回(第15〜17回)は、この区別を作るための 3 回です。本回は「Apps 面(プロセス・ファイル)を Falco で見る」を担当し、その網の目の粗さまで含めて確認します。
既習範囲 / 本回で上書きする観点
| 既習 | どこで | 本回で上書きする観点 |
|---|---|---|
| 配布元が whitelist に無いときは、足す前に別の経路を探す | 第13回(Trivy の DB) | 3 回連続で同じ判断をします。 helm repo add は Forbidden ですが OCI(ghcr.io)は通ります。whitelist は 44 行のままです |
cosign verify で署名を検証する | 第14回 | 今度は道具の内部で自動的に走ります。 falcoctl が ghcr.io からルールを取るときに cosign で署名を検証しています(Signature successfully verified!) |
| HA では 1 台だけの設定が効かない(CIS 1.2.5・ImagePolicyWebhook が 12 回中 4 回) | 第2回・第14回 | 3 回目です。今度は向きが逆です——設定は 5 台に配られたのに、1 台だけ起動しません。 「配ったこと」と「動いていること」は別です |
| PSA restricted を fanclub namespace に適用した | 第8回 | kubectl debug が 3 段階で詰まります。 restricted の要件(runAsNonRoot / allowPrivilegeEscalation: false / capabilities.drop: ALL / seccompProfile)がデバッグ手段にもそのまま効きます |
containerd の enable_selinux = true でコンテナが container_t になった | 第6回 | Falco は privileged: true なので spc_t のままです。特権コンテナは type enforcement の外にいます |
| distroless を採用しなかった判断 | 第12回 | backend には /bin/sh があります(Alpine の busybox)。だから kubectl exec -- sh が通り、Falco の検知対象になります。 シェルの有無は検知の前提条件です |
| Hubble で L7 の拒否を観測した | 第11回 | 観測する層が違います。 Hubble はネットワーク、Falco はプロセスとファイルです。第17回の 6 面の地図で両方が並びます |
| 「入れて、外す」判断(gVisor / ImagePolicyWebhook) | 第10回・第14回 | 本回は残します。 第17回・第18回が前提にするためです。ただし残すには sysctl の永続化が要ります |
先に 3 つ、本回で覆る前提を予告します。
①「helm repo add して helm install すれば Falco が入る」——本ラボでは 1 行目で止まります。 falcosecurity.github.io は whitelist に無く、Forbidden が返ります。OCI レジストリ(ghcr.io)経由なら通るので、足すのではなく、配布元を選び直します。 ②「helm install が成功したら導入は終わり」——終わりません。 DaemonSet は 5 ノードに配られますが、fs.inotify.max_user_instances を使い切っているノードでは起動しません。 実測では k8s-wl-01 だけ CrashLoopBackOff になりました。Pod が多いノードから先に枯渇するので、読者の環境では別のノードで起きます。 ③「Falco を入れれば kubectl exec は検知される」——素の kubectl exec は検知されません。 既定ルール Terminal shell in container には proc.tty != 0 という条件があり、-it を付けないと発火しません。 「記事どおりに打ったのに何も出ない」の正体はここにあります。
第3巻 19 回のうち、現在位置は次のとおりです。
第1部 第3巻オリエンテーション
第1回: 第3巻スコープ + CKA 境界 + 4C/脅威モデル + Kubestronaut
第2部 クラスタ堅牢化(D1/D2)
第2回: kube-bench + CIS ベンチマーク修復 + バイナリ検証(D1)
第3回: NetworkPolicy 完全設計 + ノードメタデータ保護 + TLS Ingress(D1)
第4回: RBAC 監査 + ServiceAccount トークン管理(D2)
第5回: API Server 堅牢化 + kubeadm アップグレード(D2)
第3部 OS / ノード堅牢化(D3)
第6回: カーネル層の強制アクセス制御(SELinux/seccomp/capabilities/AppArmor)(D3)
第7回: ホスト OS の攻撃面最小化と権限管理(D3)
第4部 ワークロード防御(D4)
第8回: Pod Security Standards + Admission(D4)
第9回: Secret 管理(etcd 暗号化 / SealedSecrets / ExternalSecrets)(D4)
第10回: マルチテナンシー分離 + サンドボックス(gVisor/RuntimeClass)(D4)
第11回: Cilium 透過暗号化 + L7 NetworkPolicy(Calico → Cilium 移行)(D4)
第5部 サプライチェーンセキュリティ(D5)
第12回: 最小イメージ + 静的解析(Kubesec + KubeLinter)(D5)
第13回: Trivy イメージスキャン + CI/CD パイプラインセキュリティ統合(D5)
第14回: SBOM(bom)+ Cosign イメージ署名 + 許可レジストリ制限(D5)
第6部 監視・ログ・ランタイムセキュリティ(D6)
★ 第15回: Falco ランタイム検知(D6) ← 今ここ
第16回: 監査ログ設計・分析 + 攻撃フェーズ特定(D6)
この回のゴール
本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。
- Falco が syscall を見ていることと、
modern_ebpf(CO-RE)が外部 probe のダウンロードを不要にする理由を説明できる - whitelist 方式のプロキシ環境で、chart の配布元を OCI へ選び直せる
helm installが成功しても DaemonSet が全ノードで起動しているとは限らないことを知り、kubectl get pods -o wideで確かめる習慣を持つError: could not initialize inotify handlerからfs.inotify.max_user_instancesに辿り着き、sysctlで直し、/etc/sysctl.d/で永続化できる- rule / macro / list の 3 要素と priority 8 段階を理解し、既定 25 本のルールが何をカバーしているかを説明できる
proc.ttyの有無で既定ルールの発火が変わることを知り、tty なしでも検知するカスタムルールを書ける- chart の
customRulesが/etc/falco/rules.d/に置かれることと、watch_config_filesによる自動リロードを説明できる - 反映経路が 2 つある(
helm upgrade= ローリング再起動 / ConfigMap 直接更新 = 自動リロード)ことを使い分けられる container.image.repositoryで絞ると同じ Pod の sidecar が漏れることを知り、k8s.pod.nameで絞る版へ書き直せる- PSA restricted の namespace で
kubectl debugが 3 段階に詰まることを知り、--profileと--customで通せる - 誤検知を「待つ」のではなく「起こしてから測る」という手順を取れる
overrideで既定ルールのチューニング用マクロを置き換え、対象のノイズだけを消せる
本回の起点
起点は第14回の完了状態です。
| 項目 | 値 |
|---|---|
| ノード | 5/5 Ready・Kubernetes v1.36.3・containerd v2.2.6・AlmaLinux 10.2 |
| Pod | 111 個・異常 0・15 namespace |
| fanclub | backend 0.4.1 × 3 / frontend 1.0.0 × 2 / postgres:18 / busybox:1.37 × 2 |
| HTTPS | https://fanclub.local/api/members が 200・会員 3 名 |
| ArgoCD | fanclub-api-prod が Synced / Healthy・HEAD は 7e39bab(手元では別の値になります・第13回の注記を参照) |
| Gateway | fanclub-gateway traefik 192.168.1.200 True |
| Longhorn | 4 ボリューム attached |
| Helm | fanclub revision 8 / cilium 1.19.6 / gatekeeper 3.23.0 ほか 12 release |
| etcd | RAFT TERM 32 で 3 台一致・leader 1・INDEX 差 3 |
| alma-proxy の whitelist | 44 行(本回も 1 行も足しません) |
| レジストリ | backend / frontend とも sha256-… 署名タグあり(第14回の成果物) |
本回で押さえておく起点条件が 5 つあります。
falconamespace は存在しません。 Falco も falcoctl も入っていません。本回で作るところから始めます- 第14回で入れた ImagePolicyWebhook は撤去済みです。apiserver の
--enable-admission-pluginsはNodeRestrictionのみに戻っています。だから本回で新しいイメージ(Falco の 3 つ)を引いても、許可レジストリの制約に当たりません - backend Pod の READY は
2/2です。内訳は本体backend(fanclub-backend:0.4.1)+ native sidecarlog-shipper(busybox:1.37)の 2 つで、これに起動時だけ動く init コンテナwait-for-db(busybox:1.37)が 1 つ加わります(init は READY に数えられません)。この構成が演習③の山になります fanclubnamespace は PSAenforce: restrictedです(第8回)。kubectl debugがそのままでは通りません- backend の probe は
httpGet(/health/live//health/ready)です。exec probe ではないので、probe 由来のノイズは出ません——これは検知の実験がやりやすいという意味で有利に働きます
本回で行き来するホスト
| ホスト | 本回での役割 |
|---|---|
| k8s-ops | helm / kubectl の実行元。Falco の chart を取得して install するのはここです |
| k8s-wl-01 | fs.inotify.max_user_instances を直すノード(演習①)。読者環境では別のノードになりえます |
| k8s-cp-01 / 02 / 03 / k8s-wl-02 | Falco の DaemonSet が乗ります。設定変更は wl-01 と同じ手順です |
| k8s-registry | 触りません(本回は新しいイメージを push しません) |
| alma-proxy | 触りません。 whitelist は 44 行のままです |
Falco は何を見ているか —— syscall とドライバ、「防ぐ層」と「気づく層」
演習に入る前に、Falco が「何を」「どこで」見ているかを 1 度で押さえます。
「防ぐ層」と「気づく層」
第2〜14回で作ってきたものは、ほぼすべて「防ぐ層」です。
防ぐ層は「起きなかったこと」を証明できません。 NetworkPolicy が通信を落としたのか、そもそも通信が発生しなかったのかは、ポリシーの側からは分かりません。気づく層は、起きたことを記録します。
そして本回で扱うのは、その気づく層にも「見ていない範囲」があるということです。 Falco は syscall を見るので、syscall を伴わない事象は 1 件も捉えません——たとえば API を叩いて Secret を読む操作は、Pod の中の syscall にはなりません。それは第16回の監査ログの担当です。
Falco は syscall を見る
Falco(CNCF)は、カーネルが発行する syscall のストリームを受け取り、ルールに合致するものを検知してアラートを出します。「コンテナ内でシェルが起動した(execve)」「/etc/shadow が読まれた(openat)」といった事象を、アプリケーションの改修なしに捉えられます。
| Falco が見えるもの | Falco が見えないもの |
|---|---|
プロセスの起動(execve)・ファイルのオープン(openat)・ネットワーク接続(connect)・ptrace・memfd_create など | Kubernetes API に対する操作(Secret の取得・RBAC の変更・Pod の作成)。これは API サーバの監査ログの担当です(第16回) |
| コンテナの中で起きたことも、ホストで起きたことも(カーネルは同じ) | 暗号化された通信の中身・アプリケーションの内部状態 |
第11回の Hubble は ネットワークのフローを見せました。Falco はプロセスとファイルを見ます。 同じ「観測」でも見ている面が違います。第17回で 6 面の地図に並べ直すので、本回は「Apps 面(プロセス・ファイル)」を担当していると覚えておいてください。
ドライバ 3 種と、本ラボで選ばれたもの
Falco が syscall を捕捉する仕組みをドライバと呼び、3 種類あります。
| ドライバ | 仕組み | 外部からのダウンロード |
|---|---|---|
kmod | カーネルモジュールをロードします | 必要(カーネルに合う .ko を取得するかビルドします) |
ebpf | 従来型の eBPF プローブ(.o ファイル) | 必要 |
modern_ebpf | CO-RE(Compile Once – Run Everywhere)。BTF 情報を使って実行時にカーネルへ合わせます | 不要 |
本ラボは chart 既定の driver.kind: auto のまま入れた結果、modern_ebpf が自動選択されました(実測)。falco-driver-loader init container のログを抜粋します。
├ type: modern_ebpf
├ target: almalinux
├ arch: x86_64
├ kernel release: 6.12.0-211.22.1.el10_2.x86_64
└ repos: https://download.falco.org/driver
INFO No artifacts needed for the selected driver.
最終行が本ラボにとって決定的です。 repos: の行には https://download.falco.org/driver が出ているのに、No artifacts needed for the selected driver. で終わっています。 つまりこの URL には 1 度もアクセスしていません。
理由は /sys/kernel/btf/vmlinux が存在することです(実測 5,652,577 バイト)。BTF(BPF Type Format・カーネル自身が持つ型情報)があると CO-RE が成立し、カーネルに合わせた成果物を外から取ってくる必要が無くなります。
実行コマンド(k8s-wl-01・developer):
$ ls -l /sys/kernel/btf/vmlinux
実行結果:
-r--r--r--. 1 root root 5652577 8月 2 00:54 /sys/kernel/btf/vmlinux
本書ラボでも当初は「第15回で download.falco.org を whitelist に追加する」と想定していましたが、不要でした。 kmod や ebpf を選んだ場合には要ります。ドライバの選択が、そのままネットワーク要件を決めています。
「auto が何を選んだか」はファイルで確認する
確定値は Pod 内のファイルに書き出されます(実測)。
/etc/falco/config.d/engine-kind-falcoctl.yaml
中身は engine: kind: modern_ebpf になっています。「auto が何を選んだか」は推測せず、このファイルで確認してください。
/sys/kernel のマウント
chart 既定の sysfsMountPath: "/sys/kernel" により、/sys/kernel/debug と /sys/kernel/tracing の両方がカバーされます(実機ではどちらも存在します)。個別にマウントを足す必要はありません。
本番ガードレール①: ドライバの選択は「速いから」ではなく「そのカーネルで成立するか」で決めます。
modern_ebpf は BTF を要求するので、BTF を持たない古いカーネルでは選べません。driver.kind: auto は環境に合わせて選んでくれますが、選ばれた結果を確認せずに本番へ出さないでください——engine-kind-falcoctl.yaml を見て、意図したドライバが選ばれていることを確かめます。
試験ではこう問われる。
「Falco をノード上で動かし、指定の挙動を検知するルールを追加せよ」という粒度で出ます。ドライバの選択そのものが問われる可能性は低いですが、「Falco が syscall を見ている」ことを前提にした設問は出ます。
falco.org/docs は試験中に参照できる 8 件のうちの 1 つなので、ドライバ名やルールのフィールド名は引けます。引けないのは「どのファイルに何を書くか」の全体像なので、そこを覚えます。
配布元を選び直す —— helm repo add は塞がれているが OCI は通る
演習①に入る前に、なぜ OCI 経路を使うのかを実測で示します。
まず塞がれていることを確認する
実行コマンド(k8s-ops・developer):
$ helm repo add falcosecurity https://falcosecurity.github.io/charts
実行結果:
Error: looks like "https://falcosecurity.github.io/charts" is not a valid chart repository
or cannot be reached: Get "https://falcosecurity.github.io/charts/index.yaml": Forbidden
curl でも確認します。
実行コマンド(k8s-ops・developer):
$ curl -s -o /dev/null -w 'code=%{http_code}\n' https://falcosecurity.github.io/charts/index.yaml
実行結果:
code=000
Forbidden を返しているのは GitHub ではなく Squid です。 code=000 は「HTTP のレスポンスが返ってこなかった」を意味します。alma-proxy の whitelist は traefik.github.io / metallb.github.io / prometheus-community.github.io のように個別に列挙する方式で、falcosecurity.github.io は入っていません。
ここで whitelist に 1 行足すのは、いちばん手軽で、いちばん考えない解決です。
第13回・第14回と同じ判断をする
第13回で Trivy の DB 配布元が塞がれたとき、whitelist に足すのではなく配布元を選び直しました。 第14回では sigstore の 3 サービス(Fulcio / Rekor / TUF)に到達できませんでしたが、鍵ペア方式で完結させて 1 行も足しませんでした。
本回も同じです。 falcosecurity.github.io は塞がれていますが、.ghcr.io は whitelist の 24 行目に登録済みです。Falco の chart は OCI アーティファクトとしても配布されているので、そちらから取れば足さずに済みます。
whitelist は 3 回連続で 44 行のままです。 出口制御のあるネットワークでは、「足す」は最後の手段であり、最初に検討することではありません。
OCI 経路で chart を確認する
実行コマンド(k8s-ops・developer):
$ helm show chart oci://ghcr.io/falcosecurity/charts/falco
実行結果(抜粋):
Pulled: ghcr.io/falcosecurity/charts/falco:9.1.0
Digest: sha256:a4e37213d1e97abc1290e026b79728facf1c1160b33935995d30be273ad17fd9
apiVersion: v2
appVersion: 0.44.1
helm show chart に --version を付けないと、そのときの最新版が引かれます。 実測では 9.1.0 が引かれましたが、install するときは必ず --version 9.1.0 でピン留めします。 Digest: の値まで控えておくと、後から「同じものを入れたか」を確認できます。
依存 chart が到達不可の URL を指していても入る
Falco の chart は 3 つの依存 chart を持ちます。
| 依存 | バージョン | 既定 | 本回での扱い |
|---|---|---|---|
| falcosidekick | 0.12.1 | disabled | 本回では有効化しません(アラート連携は第17回) |
| k8s-metacollector | 0.1.10 | condition: collectors.kubernetes.enabled | 不要(k8s_pod_name は container plugin が付けます) |
| falco-talon | 0.3.0 | disabled | 本回では扱いません |
Chart.lock を開くと repository: https://falcosecurity.github.io/charts と書いてあります。 到達できない URL です。それでも helm install は成功します。
理由は、OCI アーティファクトには依存 chart が同梱(vendored)されているからです。helm dependency update を自分で走らせる場合は Chart.lock の URL を見に行きますが、配布された成果物をそのまま入れる分には取りに行きません。
「設定ファイルに書いてある URL に必ずアクセスする」とは限りません。 到達できない URL を見つけたときは、それが実際に使われる経路かどうかを確かめてから対処してください。
使うイメージ 3 つ
| イメージ | 役割 |
|---|---|
docker.io/falcosecurity/falco | 本体(DaemonSet の falco コンテナ) |
docker.io/falcosecurity/falco-driver-loader | init container(ドライバの選択と準備) |
docker.io/falcosecurity/falcoctl | init container + sidecar(ルールとプラグインの取得・更新) |
3 つとも Docker Hub にあり、whitelist は第2巻から登録済みです。レジストリの追加も要りません。
「足すか、選び直すか」の判断
| 必要になりそうだったもの | 実機の結果 | 判断 |
|---|---|---|
falcosecurity.github.io | Forbidden / curl は code=000 | 選び直す(OCI へ) |
ghcr.io/falcosecurity/charts/falco | 通ります(.ghcr.io は登録済み) | そのまま使う |
download.falco.org | アクセスが発生しません(modern_ebpf が選ばれるため) | 不要 |
falcosecurity.github.io/falcoctl/index.yaml | 到達不可。ただし artifact は ghcr.io から取ります | 不要 |
| Docker Hub の 3 イメージ | 登録済み | 不要 |
本番ガードレール②: 出口制御のあるネットワークで新しい道具を入れるときは、「どのホストに、何のために出るか」を先に列挙します。
本回の Falco は、素朴に考えると ①chart ②イメージ ③ドライバ ④ルール の 4 系統で外へ出ます。実測すると、必要だったのは②と④だけで、④の行き先は ghcr.io でした。
入れてから Forbidden を見て 1 行ずつ足していくと、whitelist は「誰も理由を説明できない行」の集まりになります。 足す前に、足さずに済む配布経路があるかを調べてください。
試験ではこう問われる。
試験環境では Falco が既に入っている構成で出題される可能性が高いので、導入経路そのものは問われにくいところです。ただし「chart のバージョンをピン留めする」「OCI レジストリから chart を取る」は実務では日常的に必要になります。
helm.sh/docs は CKS では参照できません(CKAD / CKA では参照可)。Helm のフラグは覚えておく必要があります——本回で使うのは helm install <name> oci://<ref> --version <ver> -n <ns> --create-namespace と helm upgrade -f values.yaml の 2 つだけです。
やってみよう①:Falco を導入し、1 台だけ上がらない事象を切り分ける
所要時間の目安: 約 15 分(Pod の起動待ちとイメージの pull を含みます)。chart を OCI から入れ、起動していないノードを特定し、原因を追い、直し、永続化します。試験相当の作業(values.yaml に設定を書いて helm upgrade する)なら 3 分です。
この演習が本回でいちばん長いのには理由があります。 本回は「入れて終わり」にしません。 検知の道具は、それ自身が動いていることを確認できないと意味がありません。 5 台のうち 1 台で動いていなければ、そのノードに乗っている Pod は 1 件も監視されていません。 しかもアラートが出ないだけなので、静かなまま気づけません。
先に断っておきます。
この演習で扱う OCI 経路・sysctl・永続化は、いずれも本書ラボの事情です。
試験では Falco は導入済みの状態で出題されます。 出口制御プロキシも無いので helm repo add は普通に通りますし、上限に当たったノードを切り分ける場面も出ません。試験相当の作業は「ルールを追加して反映させる」の 3 分だけです。
それでもこの 15 分を通す理由は、検知の道具が動いていないことに気づく手段を持つためです。試験では問われませんが、現場では毎回問われます。どこが試験でどこがラボかの切り分けは、本回の後半に表としてまとめます。
ステップ1:現状を確認する
実行コマンド(k8s-ops・developer):
$ kubectl get ns falco
実行結果:
Error from server (NotFound): namespaces "falco" not found
ステップ2:OCI 経路で導入する
実行コマンド(k8s-ops・developer):
$ helm install falco oci://ghcr.io/falcosecurity/charts/falco --version 9.1.0 --namespace falco --create-namespace
実行結果(抜粋):
Pulled: ghcr.io/falcosecurity/charts/falco:9.1.0
Digest: sha256:a4e37213d1e97abc1290e026b79728facf1c1160b33935995d30be273ad17fd9
NAME: falco
NAMESPACE: falco
STATUS: deployed
REVISION: 1
実測値です。
| 項目 | 実測 |
|---|---|
helm install の所要 | 2 秒(追加フラグなし) |
| 追加した values | なし(既定のまま) |
helm install は 2 秒で返ります。 これは「chart を適用した」だけであって、Pod が起動したという意味ではありません。 Helm の STATUS: deployed を導入完了の合図にしないでください——確認するのは kubectl get pods の側です。
ステップ3:5 台中 4 台しか上がっていないことを発見する
実行コマンド(k8s-ops・developer):
$ kubectl get pods -n falco -o wide
実行結果(実測の要点):
| ノード | 状態 |
|---|---|
| k8s-cp-01 | Running |
| k8s-cp-02 | Running |
| k8s-cp-03 | Running |
| k8s-wl-02 | Running |
| k8s-wl-01 | CrashLoopBackOff |
helm install は成功しています。DaemonSet は 5 ノード分の Pod を作っています。それでも 1 台は動いていません。
第14回の ImagePolicyWebhook では「1 台にしか設定を入れていないので 12 回中 4 回しか効かない」でした。今回は逆で、5 台すべてに配ったのに 1 台だけ効いていません。 方向は違いますが、確かめずに「入った」と判断すると同じ結果になるという点で同じです。
DaemonSet を入れたら -o wide で全ノード分を数えてください。 これは本回に限った話ではありません。
読者の環境で 5 台とも Running だった場合。
そのノードの Pod 密度が本書ラボより低いというだけで、何も間違っていません。 上限に当たるかどうかは、そのノードに何個の Pod が載っているかで決まります(理由はステップ 5 で数えます)。
- ステップ 4〜6 は読み物として目を通してください。 「どう切り分けるか」がこの演習の中身なので、再現しなくても筋道は追えます
- ステップ 5(上限と使用量を数える)は必ず打ってください。 自分のノードが上限にどれだけ近いかが分かります。残り 10 を切っていれば、次に Pod が数個増えた時点で同じことが起きます
- ステップ 7(永続化)は必ず実施してください。 ここを飛ばすと、第16回以降で Pod が増えたときに、今度は別のノードで同じ事象が起きます
逆に 2 台以上が CrashLoopBackOff になった場合は、同じ手順を該当ノードすべてで繰り返します。手順は変わりません。
ステップ4:原因を読む
実行コマンド(k8s-ops・developer):
$ kubectl get pods -n falco --no-headers | awk '$3!="Running"{print $1}'
実行結果(実測):
falco-n6kd2
この Pod を名指しでログを読みます。
実行コマンド(k8s-ops・developer):
$ kubectl logs -n falco falco-n6kd2 -c falco --tail=8
実行結果(実測・末尾 8 行):
Sat Aug 01 16:07:10 2026: [libs]: container: Enabled 'bpm' container engine.
Sat Aug 01 16:07:10 2026: Loading rules from:
Sat Aug 01 16:07:10 2026: /etc/falco/falco_rules.yaml | schema validation: ok
Sat Aug 01 16:07:10 2026: Hostname value has been overridden via environment variable to: k8s-wl-01
Error: could not initialize inotify handler
Events detected: 0
Rule counts by severity:
Triggered rules by rule name:
ここでラベルセレクタを使わない理由。
kubectl logs -l app.kubernetes.io/name=falco --tail=20 --prefix と打つと 5 台分で 100 行出ます(実測)。目的の Error: はその中の 1 行で、しかも他のノードが出しているアラートは1 件が長大な 1 行なので、画面上でまず見つかりません。
落ちている Pod が分かっているなら、名指しするほうが速いです。 ステップ 3 で「どのノードか」まで特定してあるので、その情報をそのまま使います。
エラーは 1 行しかありません。 inotify という単語があるので、カーネルの inotify に関する上限に当たっていると当たりを付けます。その直前まで、ルールの読み込みは成功しています——つまり設定やルールの問題ではありません。
inotify は、ファイルやディレクトリの変更をカーネルがプログラムへ通知する仕組みです(inode notify の略)。監視したいプログラムは「インスタンス」を 1 つ作り、その中に「ウォッチ」を必要なだけ登録します。上限はこの 2 つに別々にかかります。
Falco が inotify を使うのは watch_config_files: true(既定)のためです——設定ファイルとルールファイルの変更を監視して自動リロードするのに使っています。この機能は本回の演習③・④でそのまま使うので、外して回避する道は取りません。
ステップ5:上限と使用量を数える
実行コマンド(k8s-wl-01・developer):
$ sysctl fs.inotify.max_user_instances fs.inotify.max_user_watches
実行結果:
fs.inotify.max_user_instances = 128
fs.inotify.max_user_watches = 60255
使用中のインスタンス数は、プロセスが開いているファイルディスクリプタを数えて求めます。
実行コマンド(k8s-wl-01・root):
# find /proc/*/fd -lname anon_inode:inotify 2>/dev/null | wc -l
実行結果(実測):
144
上限 128 に対して 144 です。 3 ノードで比べます(実測)。
| ノード | max_user_instances | 使用中インスタンス | containerd-shim の数 | 結果 |
|---|---|---|---|---|
| k8s-cp-01 | 128 | 67 | 13 | 起動 |
| k8s-wl-02 | 128 | 96 | 24 | 起動 |
| k8s-wl-01 | 128 | 144 | 52 | 起動不可 |
containerd-shim の数と使用量がきれいに対応しています。 shim の個数は、ノードに載っている Pod の数をそのまま表します。
内訳です(実測)。
| プロセス | 1 プロセスあたりのインスタンス |
|---|---|
containerd-shim | 2〜4(これが効きます) |
kubelet | 6 |
systemd | 5 + 3 |
polkitd | 2 |
gpg-agent | 2 |
containerd-shim は 1 Pod あたり 1 プロセスで、それぞれ 2〜4 インスタンス使います(Pod の構成によって変わります)。1 つあたりの数より、プロセスの数が効きます——つまり Pod が多いノードほど先に枯渇します。
k8s-wl-01 は 144 で、上限 128 を既に超えています。 超えた状態でも既存のプロセスは動き続けますが、新しく inotify を要求したプロセスが失敗します。 そこに Falco が来ました。
だから「Falco を入れたら 1 台だけ上がらない」は、Falco の問題ではありません。 ノードの Pod 密度の問題です。読者の環境では別のノードで起きますし、Pod を減らせば起きません。
max_user_watches は 60,255 で余裕があります(実測)。枯渇しているのは instances のほうです——名前が似ているので取り違えやすいところです。
しかも max_user_watches はノードごとに値が違います。 実測では k8s-cp-01 が 28,471・Workload の 2 台が 60,255 でした。カーネルがメモリ量から自動算出するためです。一方 max_user_instances は全ノード一律 128(ディストロ既定のまま)です。
「自分のノードで打ったら記事と違う数字が出た」となるのは watches のほうで、それは正常です。 見るべきは instances の残り——上限 128 に対して今いくつ使っているか、です。

ステップ6:直す(一時的)
実行コマンド(k8s-wl-01・root):
# sysctl -w fs.inotify.max_user_instances=512
実行結果:
fs.inotify.max_user_instances = 512
続けて Pod を作り直します。
実行コマンド(k8s-ops・developer):
$ kubectl delete pod -n falco --field-selector spec.nodeName=k8s-wl-01
実測値です。
| 項目 | 実測 |
|---|---|
Pod 削除から Running まで | 45〜61 秒(2 回計測) |
1 分前後かかるのは、init container が 2 つ動くからです(falco-driver-loader と falcoctl-artifact-install)。CrashLoopBackOff からの復帰ではなく、Pod を作り直しています——sysctl の値は起動時に読まれるので、プロセスを作り直さないと反映されません。
ステップ7:永続化する(この演習を「残す」ための必須手順)
sysctl -w はメモリ上の値を書き換えるだけで、ノードを再起動すると戻ります。
実行コマンド(k8s-wl-01・root):
# echo 'fs.inotify.max_user_instances = 512' > /etc/sysctl.d/90-falco-inotify.conf
# sysctl --system
実行結果:
* Applying /usr/lib/sysctl.d/10-default-yama-scope.conf ...
* Applying /usr/lib/sysctl.d/10-map-count.conf ...
* Applying /usr/lib/sysctl.d/50-coredump.conf ...
* Applying /usr/lib/sysctl.d/50-default.conf ...
* Applying /usr/lib/sysctl.d/50-libkcapi-optmem_max.conf ...
* Applying /usr/lib/sysctl.d/50-pid-max.conf ...
* Applying /usr/lib/sysctl.d/50-redhat.conf ...
* Applying /etc/sysctl.d/90-falco-inotify.conf ...
* Applying /etc/sysctl.d/99-sysctl.conf ...
* Applying /etc/sysctl.d/99-zzz-override_cilium.conf ...
* Applying /etc/sysctl.d/k8s.conf ...
* Applying /etc/sysctl.conf ...
掲載したのは先頭 12 行だけです。
実際の出力は 309 行あります(実測)。13 行目から先は適用された値そのものが延々と並び、その大半は 第11回で入れた Cilium が作った lxc* インターフェースの設定です。Pod が増減するたびに変わるので、読者の画面とは一致しません。
見るべきはこの 12 行のほうです。 * Applying … が読み込まれた順に並ぶので、90-falco-inotify.conf が 99-* や k8s.conf より先に適用されていることが読めます。数字の接頭辞は読み込み順を決めるためにあります——同じ値を複数のファイルが設定していたら、後に読まれたほうが勝ちます。
既存のファイルに追記しないでください。 /etc/sysctl.d/ には本書ラボの時点で 3 つのファイルがありますが、いずれも出所が違います。
| ファイル | 中身 | 出所 |
|---|---|---|
k8s.conf | net.bridge.bridge-nf-call-iptables ほか 3 行 | 第2巻の kubeadm 前提設定 |
99-zzz-override_cilium.conf | net.ipv4.conf.lxc*.rp_filter ほか | 第11回で Cilium が自動生成したもの |
99-sysctl.conf | 説明コメントのみ | ディストロ既定 |
ここに Falco の都合を混ぜると、半年後に「この行は誰が何のために入れたのか」が読めなくなります。 とくに 99-zzz-override_cilium.conf は Cilium が管理しているファイルなので、書き足しても次の helm upgrade で消えることがあります。用途ごとに 1 ファイルを作るのが正しい形です。
>>(追記)ではなく >(上書き)を使う理由も同じところにあります。演習をやり直したときに同じ行が二重に入りません。 設定ファイルを配る操作は、何回打っても同じ状態になる形で書きます。
確認します。
実行コマンド(k8s-wl-01・developer):
$ sysctl fs.inotify.max_user_instances
実行結果:
fs.inotify.max_user_instances = 512
本回で入れた Falco は、第17回と第18回が土台にします。だから残します。
残すということは、ノードを再起動しても動く状態にするということです。 sysctl -w のままにすると、次にノードが再起動したときに 1 台だけ Falco が上がらない状態へ戻ります。 そしてそのときには、本回の記憶は薄れています。
第10回の gVisor は撤去しました。第14回の ImagePolicyWebhook も撤去しました。本回は残します。 判断が違うのは、後続の回が前提にするかどうかという 1 点です。残す判断をしたなら、残せる状態にするまでが作業です。
本番ガードレール③: 1 台だけ直した状態を正にしません。
本書ラボでは上限に当たったのが k8s-wl-01 の 1 台だけなので、演習でもそのノードだけを直しています。本番では、この状態を正としません。
上限はノード全体で揃えます。 理由は 2 つあります。①どのノードで枯渇するかは Pod の置かれ方で決まり、スケジューラの都合で明日には別のノードになります ②ノードを増やしたときに、新しいノードだけ設定が無い状態が生まれます。
本回は第14回で「1 台だけに入れた Admission は 12 回中 4 回しか効かなかった」を見ています。 あれは「効かない」形で表に出ましたが、カーネルパラメータの不揃いは、そのノードに Pod が寄るまで何も起きません。 気づくのは、いつもクラスタが混んでいるときです。
配る手段は構成管理(Ansible / Ignition / ノードイメージ)で、/etc/sysctl.d/90-falco-inotify.conf を全ノードへ同じ内容で置くのが定石です。
ステップ8:5 台すべてで Running を確認する
実行コマンド(k8s-ops・developer):
$ kubectl get pods -n falco -o wide
実行結果:
NAME READY STATUS RESTARTS AGE NODE
falco-xxxxx 2/2 Running 0 Xm k8s-cp-01
falco-xxxxx 2/2 Running 0 Xm k8s-cp-02
falco-xxxxx 2/2 Running 0 Xm k8s-cp-03
falco-xxxxx 2/2 Running 0 Xm k8s-wl-01
falco-xxxxx 2/2 Running 0 Xm k8s-wl-02
READY が 2/2 なのは、1 つの Pod に falco と falcoctl-artifact-follow の 2 コンテナが入っているからです(この後の「falcoctl は 2 つの形で入っている」の節で扱います)。1/1 を期待して見ると、増えているように読めますが正常です。
ステップ9:検知基盤自身の負荷を測る
実行コマンド(k8s-ops・developer):
$ kubectl top pods -n falco
実行結果(実測):
NAME CPU(cores) MEMORY(bytes)
falco-fj44c 5m 112Mi
falco-g5z26 14m 147Mi
falco-kqjrz 11m 154Mi
falco-n9288 5m 106Mi
falco-zk9hd 4m 109Mi
5 行に満たないことがあります。
Pod を作り直した直後はメトリクスがまだ集まっていないので、その台だけ行が出ません(実測でも 4 行のことがありました)。数分待てば揃います。
まとめると次のとおりです(実測)。
| 項目 | 実測 |
|---|---|
| CPU | 4〜14m(5 台) |
| メモリ | 106〜154Mi(5 台) |
| syscall の drop | 0 件 |
syscall の drop が 0 件であることを確認しておきます。 Falco はカーネルからのイベントをリングバッファで受け取るので、負荷が高いと取りこぼします。 取りこぼした分はアラートが出ません——つまり「静かだから安全」の 3 つ目の原因になります。
本ラボの規模(111 Pod・5 ノード)では 0 件でした。 本番では定期的に確認する項目です。
演習①のゴール確認
| 確認 | 期待 |
|---|---|
falco namespace の Pod | 5 台とも Running / 1/1 |
fs.inotify.max_user_instances(k8s-wl-01) | 512・/etc/sysctl.d/ に永続化済み |
| 選ばれたドライバ | modern_ebpf(engine-kind-falcoctl.yaml で確認) |
| whitelist | 44 行のまま |
| 既存の 111 Pod | 異常 0 のまま |
https://fanclub.local/api/members | 200 + 会員 3 名 |
| CPU / メモリ | 4〜14m / 106〜154Mi |
| syscall drop | 0 件 |
本番ガードレール④: 検知基盤は、動いていることを能動的に確認する対象です。
アプリケーションは止まれば誰かが気づきますが、検知基盤は止まっても静かになるだけです。
最低限、次の 3 つを監視項目に入れます——①DaemonSet の desiredNumberScheduled と numberReady が一致しているか ②syscall の drop 件数 ③ルールのリロードが成功しているか。 第2巻で構築した Prometheus に載せる話は第17回で扱います。
試験ではこう問われる。
試験では Falco が既に導入された状態で出題されます。 inotify の枯渇も sysctl も試験では出ません(ラボ固有の事情です)。
試験相当の作業は「Falco が動いているノードで、指定のルールを追加して反映させる」であり、3 分程度で終わる粒度です。本演習が 15 分かかるのは、閉じたネットワークで同じ環境を作るためです。
ただし「DaemonSet が全ノードで Ready か確認する」という所作は試験でも役に立ちます。 「Falco が検知しない」という設問で、そもそもそのノードで動いていないという筋は実際にありえます。
falcoctl はルールを ghcr.io から取り、cosign で検証している —— 第14回が道具の内側にある
導入直後に流れているログを読み解きます。第14回で手で打った cosign verify が、ここでは道具の内部で自動的に走っています。
falcoctl が何をしているか
Falco の Pod には falco 以外に falcoctl が 2 つの形で入っています。
| 形 | 役割 |
|---|---|
init container(falcoctl-artifact-install) | 起動時にルールとプラグインを取得します |
sidecar(falcoctl-artifact-follow) | 稼働中にルールの更新を追います |
実行コマンド(k8s-ops・developer):
$ kubectl logs -n falco <falco Pod 名> -c falcoctl-artifact-install
実行結果(抜粋):
{"level":"INFO","msg":"Resolving dependencies ..."}
{"level":"INFO","msg":"Installing artifacts","refs":["ghcr.io/falcosecurity/rules/falco-rules:5","ghcr.io/falcosecurity/plugins/plugin/container:0.7.1"]}
{"level":"INFO","msg":"Preparing to pull artifact","ref":"ghcr.io/falcosecurity/rules/falco-rules:5"}
{"digest":"ghcr.io/falcosecurity/rules/falco-rules@sha256:36d143c0ae2d...","level":"INFO","msg":"Verifying signature for artifact"}
{"level":"INFO","msg":"Signature successfully verified!"}
{"directory":"/rulesfiles","level":"INFO","msg":"Artifact successfully installed","name":"ghcr.io/falcosecurity/rules/falco-rules:5","type":"rulesfile"}
2 つの artifact(ルールとプラグイン)を取る順序は、実行のたびに入れ替わります(実測で両方の順序を確認しました)。どちらが先かは気にしなくて構いません。
第14回との接続
第14回で、読者は cosign generate-key-pair して cosign sign して cosign verify を打ちました。 そのとき扱ったのは自分たちのイメージと自分たちの鍵でした。
ここで起きているのは、同じことを falcoctl が自動でやっているという事実です。ghcr.io/falcosecurity/rules/falco-rules:5 を取ってきて、その署名を検証してから展開しています。
第14回の主題「署名検証は『署名があるか』ではなく『誰の署名か』を見る」がそのまま効いています。 falcoctl が検証しているのは Falco プロジェクトの署名であり、自分たちの鍵ではありません。 道具の中に、Falco プロジェクトを信頼するという判断が埋め込まれています。
つまりサプライチェーンは、道具を入れた時点で 1 段深くなります。 自分たちのイメージに署名しても、その署名を検証する道具自身がどこから来たかという問いは残ります。第14回で「持ち込むもの」と「出すもの」を区別しましたが、ここでは「持ち込んだ道具が、さらに何かを持ち込んでいる」という形になっています。
index には到達できないのに install は成功する
falcoctl の index は https://falcosecurity.github.io/falcoctl/index.yaml を指しており、これは到達できません(falcosecurity.github.io が whitelist に無いためです)。それでも artifact の取得は成功します。
| 何 | 行き先 | 到達 |
|---|---|---|
| falcoctl の index(名前 → OCI 参照の対応表) | falcosecurity.github.io/falcoctl/index.yaml | 不可 |
| artifact 本体(ルール・プラグイン) | ghcr.io/falcosecurity/... | 可 |
chart の既定値が、artifact を OCI 参照で直接指定しているため、index を引く必要がありません。index は「falco-rules という名前がどの OCI 参照に対応するか」を調べるためのものであり、参照が最初から書いてあれば不要です。
前節の Chart.lock と同じ構図です——設定に書いてある URL が、必ず使われるとは限りません。 ログに Forbidden が出ていないことを確認したうえで、到達できない URL を放置する判断も成り立ちます。
取得されるもの 2 つ
| artifact | バージョン | 中身 |
|---|---|---|
ghcr.io/falcosecurity/rules/falco-rules | 5 | 既定ルール(/etc/falco/falco_rules.yaml・63,642 バイト) |
ghcr.io/falcosecurity/plugins/plugin/container | 0.7.1 | container プラグイン(container_id / container_image_repository / k8s_pod_name / k8s_ns_name などを付けます) |
アラートに k8s_pod_name と k8s_ns_name が入るのは、container プラグイン 0.7.1 が付けているからです。chart の依存にある k8s-metacollector を有効化しなくても、Pod 名と namespace は出ます。
本回では 3 つの依存 chart をどれも有効化しません。 アラートの転送は第17回の担当です。本回は「検知が出ること」までを扱います。
本回で導入されるもの一覧
| 要素 | バージョン | 取得元 |
|---|---|---|
| Falco 本体 | 0.44.1 | docker.io/falcosecurity/falco |
| Helm chart | 9.1.0 | oci://ghcr.io/falcosecurity/charts/falco |
| falcoctl | 0.13.0 | docker.io/falcosecurity/falcoctl |
| ドライバローダ | (本体と同系列) | docker.io/falcosecurity/falco-driver-loader |
| container プラグイン | 0.7.1 | ghcr.io/falcosecurity/plugins/plugin/container |
| 既定ルール | falco-rules:5 | ghcr.io/falcosecurity/rules/falco-rules |
| Libs | 0.25.4 | (本体に同梱) |
| Plugin API | 3.12.0 | (本体に同梱) |
Pod の中で直接確認できます。
実行コマンド(k8s-ops・developer):
$ kubectl exec -n falco <falco Pod 名> -c falco -- falco --version
実行結果(実測・末尾 8 行):
Falco version: 0.44.1
Libs version: 0.25.4
Plugin API: 3.12.0
Engine: 0.62.0
Driver:
API version: 10.0.0
Schema version: 4.3.0
Default driver: 10.2.0+driver
先頭には設定ファイルの読み込み結果が数行出ます(falco --version でも設定を読むためです)。見たいのはこの末尾 8 行です。
本番ガードレール⑤: 道具が自動で外から取ってくるものを把握します。
本回の Falco は、起動のたびに ghcr.io から 2 つの artifact を取ります。 つまり ghcr.io が落ちていると Pod が起動しない可能性があります。
本番では、①artifact をバージョンで固定する ②自社レジストリにミラーする、のどちらかを検討します。 「入れたときは動いた」は、次に Pod が作り直されるときの動作を保証しません——第14回の「絞った直後には何も起きない」と同じ性質です。
既定ルールは 25 本しかない —— rule / macro / list と priority
演習②に入る前に、ルールの文法と、既定でカバーされている範囲を確認します。
ルールを構成する 3 要素
| 要素 | 役割 | 書き方 |
|---|---|---|
rule | 検知条件(condition)・出力(output)・優先度(priority)を定義します | - rule: <名前> |
macro | 条件の部品(再利用できる式) | - macro: <名前> + condition: |
list | 値の集合(プロセス名の一覧など) | - list: <名前> + items: [...] |
priority は 8 段階です。
EMERGENCY > ALERT > CRITICAL > ERROR > WARNING > NOTICE > INFO > DEBUG
本ラボの実測(falco.yaml の設定)です。
| 設定 | 実測値 | 意味 |
|---|---|---|
priority(出力しきい値) | debug | 全優先度を出します(絞っていません) |
rule_matching | first | 最初にマッチしたルールで打ち切ります |
watch_config_files | true(既定) | 設定・ルールの変更を監視して自動リロードします |
rules_files の順序 | falco_rules.yaml → falco_rules.local.yaml → rules.d | 後から読まれたものが後勝ちで上書きできます |
rule_matching: first の意味を押さえておきます。 1 つの syscall イベントに複数のルールが当てはまる場合、最初にマッチした 1 本で打ち切られます。 つまり「同じ操作で 2 本のアラートが出る」ことは既定では起きません。
これは演習③で効いてきます——カスタムルールと既定の Terminal shell in container の両方に当てはまる操作をしたとき、どちらか一方しか出ません。 「カスタムルールが効いていない」と読み違えないよう、rules_files の読み込み順と rule_matching を先に確認しておきます。
既定ルールは 25 本
falco-rules:5 が入れる /etc/falco/falco_rules.yaml(実測 63,642 バイト)に含まれる - rule: は 25 本です(実測)。
Directory traversal monitored file read
Read sensitive file trusted after startup
Read sensitive file untrusted
Run shell untrusted
System user interactive
Terminal shell in container
Contact K8S API Server From Container
Netcat Remote Code Execution in Container
Search Private Keys or Passwords
Clear Log Activities
Remove Bulk Data from Disk
Create Symlink Over Sensitive Files
Create Hardlink Over Sensitive Files
Packet socket created in container
Redirect STDOUT/STDIN to Network Connection in Container
Linux Kernel Module Injection Detected
Debugfs Launched in Privileged Container
Detect release_agent File Container Escapes
PTRACE attached to process
PTRACE anti-debug attempt
Find AWS Credentials
Execution from /dev/shm
Drop and execute new binary in container
Disallowed SSH Connection Non Standard Port
Fileless execution via memfd_create
実行コマンド(k8s-ops・developer):
$ kubectl exec -n falco <falco Pod 名> -c falco -- grep -c '^- rule:' /etc/falco/falco_rules.yaml
実行結果:
25
25 本です。 これを多いと見るか少ないと見るかは、何を守りたいかによります。
既定ルールは「どんな環境でも誤検知が出にくいもの」に絞られています。 環境固有の事情(このイメージだけは exec を許す、このプロセスは正常)を知らない状態で配られるので、広く網を張ると誤検知だらけになります。 だから狭く始めて、環境ごとに足していくのが前提の設計です。
「Falco を入れれば検知される」ではなく、「Falco を入れると 25 本の網が張られる。足りない分は自分で書く」が正しい理解です。カスタムルールを書くことは、追加作業ではなく本来の使い方です。
25 本が見ている範囲
| 分類 | 該当ルール |
|---|---|
| シェルとプロセス | Terminal shell in container / Run shell untrusted / System user interactive / Drop and execute new binary in container / Execution from /dev/shm / Fileless execution via memfd_create |
| 機密ファイルの読み取り | Read sensitive file untrusted / Read sensitive file trusted after startup / Directory traversal monitored file read / Search Private Keys or Passwords / Find AWS Credentials |
| ネットワーク | Contact K8S API Server From Container / Netcat Remote Code Execution in Container / Redirect STDOUT/STDIN to Network Connection in Container / Packet socket created in container / Disallowed SSH Connection Non Standard Port |
| 痕跡の消去・破壊 | Clear Log Activities / Remove Bulk Data from Disk / Create Symlink Over Sensitive Files / Create Hardlink Over Sensitive Files |
| コンテナからの脱出・カーネル操作 | Detect release_agent File Container Escapes / Linux Kernel Module Injection Detected / Debugfs Launched in Privileged Container |
| デバッガの悪用 | PTRACE attached to process / PTRACE anti-debug attempt |
分類してみると、攻撃の流れに沿って並んでいることが分かります——入る(シェル)→ 調べる(機密ファイル)→ 外へ出す(ネットワーク)→ 消す(痕跡)→ 上がる(脱出・カーネル)。第18回の DAIR で扱う攻撃フェーズと対応します。
Terminal shell in container の条件に proc.tty != 0 がある
演習②の伏線として、この 1 本だけ条件を読みます。
実行コマンド(k8s-ops・developer):
$ kubectl exec -n falco <falco Pod 名> -c falco -- grep -A 12 'rule: Terminal shell in container' /etc/falco/falco_rules.yaml
実行結果(実測):
- rule: Terminal shell in container
desc: >
A shell was used as the entrypoint/exec point into a container with an attached terminal. Parent process may have
legitimately already exited and be null (read container_entrypoint macro). Common when using "kubectl exec" in Kubernetes.
Correlate with k8saudit exec logs if possible to find user or serviceaccount token used (fuzzy correlation by namespace and pod name).
Rather than considering it a standalone rule, it may be best used as generic auditing rule while examining other triggered
rules in this container/tty.
condition: >
spawned_process
and container
and shell_procs
and proc.tty != 0
and container_entrypoint
grep -A 12 なので、ここまでが 12 行分です。 output と priority は続きにあります(-A の数を増やせば読めます)。本節で見たいのは condition の 5 行です。
ルール名は Terminal shell in container です。 「Shell in container」ではなく 「Terminal shell」——端末が付いているシェルを検知する、と名前に書いてあります。
条件にも proc.tty != 0 があります。 proc.tty は制御端末のデバイス番号で、端末が付いていなければ 0 になります。つまり kubectl exec に -it を付けなければ、このルールには当たりません。
これは実装の不備ではなく、意図された絞り込みです。 コンテナの中では、アプリケーションが子プロセスとしてシェルを起動することが日常的にあります(起動スクリプト・ヘルスチェック・cron)。それらを全部拾うと誤検知だらけになるので、「人が対話的に入ってきた」ものだけに絞っています。
絞り込みは、常に「見なくなるもの」を作ります。 次の演習で、その見なくなった側を確認します。
本回で使う主なフィールド
| フィールド | 意味 | 本回での用途 |
|---|---|---|
proc.name | プロセス名 | シェルの検知 |
proc.exepath | 実行ファイルの実体パス | sh の実体が /bin/busybox であることの確認 |
proc.tty | 制御端末のデバイス番号(無ければ 0) | 演習②の中心 |
proc.cmdline | コマンドライン全体 | 何をしたかの記録 |
user.name / user.uid | 実行ユーザー | appuser / 10001 |
container.image.repository | イメージのリポジトリ名 | 演習③で「絞りすぎる」側 |
container.image.tag | イメージのタグ | 0.4.1 |
k8s.pod.name | Pod 名 | 演習③で「正しく絞る」側 |
k8s.ns.name | namespace 名 | fanclub |
spawned_process | マクロ(プロセスが起動した) | カスタムルールの起点 |
container | マクロ(コンテナ内である) | カスタムルールの起点 |
本番ガードレール⑥: ルールを読まずに「入れたから検知される」と考えません。
既定ルールは 25 本で、それぞれに絞り込みの条件が入っています。
自分の環境で守りたい挙動が、既定 25 本のどれかで拾われるかを 1 度は確かめます。拾われないなら書きます。 そして書いたルールについても、「何を見て、何を見ていないか」を同じように確かめます——これは本回の演習③でそのまま確認します。
試験ではこう問われる。
「指定のディレクトリへの書き込みを検知するルールを作れ」「既存のルールの優先度を変更せよ」という粒度で出ます。手が覚えているべきは rule / macro / list の 3 要素と、condition / output / priority / tags のキー名です。
falco.org/docs は試験中に参照できるので、フィールド名(proc.name / fd.name / container.id など)の一覧は引けます。 引く時間を減らすために、よく使う 10 個程度は覚えておきます。
やってみよう②:kubectl exec が検知されないことを確認する
所要時間の目安: 約 6 分。既定ルールに対して 2 通りの kubectl exec を打ち、アラートの有無を比べます。試験相当の作業(検知されるか確認する)なら 3 分です。
読者が最初につまずくのはここです。 記事や公式ドキュメントの手順どおりに kubectl exec を打っても、アラートが 1 件も出ません。 そこで「Falco が壊れている」「導入に失敗した」と考えて、動いているものを疑って時間を使います。
本演習は、先に「出ない」ことを確認してから、「出る」ことを確認します。 順序が逆だと学びになりません。
ステップ0:アラートの見方を用意する
実行コマンド(k8s-ops・developer・別の端末で流しっぱなしにします):
$ kubectl logs -n falco -l app.kubernetes.io/name=falco -c falco -f --prefix --tail=0
--prefix を付けると、どの Pod が出したアラートかが行頭に付きます。 DaemonSet なので 5 台分のログが混ざります——アラートが「どのノードで起きたか」は、この接頭辞で分かります。
--tail=0 で「これ以降」に絞ります。 起動直後のログを同時に流すと、falcoctl の取得ログや起動時の誤検知(演習④で扱います)が混ざって読みにくくなります。
ステップ1:素の kubectl exec を打つ(tty なし)
実行コマンド(k8s-ops・developer):
$ kubectl exec -n fanclub <backend Pod 名> -c backend -- sh -c 'id'
実行結果:
uid=10001(appuser) gid=10001(appuser) groups=10001(appuser)
このときの Falco のログ:
(1 件も出ません)
コンテナの中でシェルが起動して、コマンドが実行されて、結果が返ってきました。それでも Falco は 1 件も出しません。
原因は前節で読んだとおり、Terminal shell in container の条件に proc.tty != 0 があるからです。 -it を付けない kubectl exec は端末を割り当てないので、proc.tty が 0 になり、条件に当たりません。
そして残り 24 本のルールにも当たりません。 id を打っただけでは、機密ファイルも読んでいませんしネットワークにも出ていません。25 本の網は、この操作を 1 度も捉えません。
ステップ2:-it を付けて打つ
実行コマンド(k8s-ops・developer):
$ kubectl exec -it -n fanclub <backend Pod 名> -c backend -- sh -c 'id'
実行結果:
uid=10001(appuser) gid=10001(appuser) groups=10001(appuser)
Falco のログ(実測・抜粋):
13:43:31.320992342: Notice A shell was spawned in a container with an attached terminal |
evt_type=execve user=appuser user_uid=10001 user_loginuid=-1 process=sh
proc_exepath=/bin/busybox parent=containerd-shim command=sh -c id terminal=34816
exe_flags=EXE_LOWER_LAYER container_id=2c1d3c8630bd container_name=backend
container_image_repository=k8s-registry:5000/fanclub-backend container_image_tag=0.4.1
k8s_pod_name=fanclub-backend-56875d65c9-m5q8c k8s_ns_name=fanclub
ステップ3:出力を 1 フィールドずつ読む
| フィールド | 実測値 | 読み方 |
|---|---|---|
Notice | — | priority が NOTICE(8 段階の下から 3 番目) |
evt_type | execve | プロセスの起動という syscall |
user / user_uid | appuser / 10001 | 第6回・第8回で runAsNonRoot にした結果がここに出ます |
user_loginuid | -1 | ログイン経由ではありません(対話ログインなら uid が入ります) |
process | sh | プロセス名 |
proc_exepath | /bin/busybox | 実体は busybox です(Alpine の sh は busybox へのリンク) |
parent | containerd-shim | 親プロセスがコンテナランタイム——つまり外から差し込まれたことを意味します |
command | sh -c id | 打ったコマンドがそのまま出ます |
terminal | 34816 | 0 ではありません。これが発火の条件でした |
container_image_repository / _tag | k8s-registry:5000/fanclub-backend / 0.4.1 | 第13回でビルドし第14回で署名したタグがそのまま出ます |
k8s_pod_name / k8s_ns_name | Pod 名 / fanclub | container プラグイン 0.7.1 が付けています |
表記の違いに戸惑わないでください。 ルールに書くときはドット区切り(k8s.pod.name / container.image.repository)、アラートに出るときはアンダースコア区切り(k8s_pod_name / container_image_repository)です。 同じフィールドの表記違いにすぎません。 次の演習でルールを書くときはドット区切りを使います。
親プロセスが containerd-shim であるという 1 点が、実務では効きます。アプリケーションが自分で起動したシェルなら、親はアプリケーションのプロセスになります。 親がコンテナランタイムということは、コンテナの外から exec で差し込まれたという意味です。
第16回で扱う監査ログには「誰が kubectl exec を打ったか」が記録されます。 Falco 側には「誰が」は出ません——コンテナの中から見ると、外の人間は見えないからです。2 つを突き合わせて初めて「誰が、何をしたか」が揃います。 これが第18回の DAIR で使う手順になります。
ステップ4:2 つの結果を並べる
| 打ち方 | proc.tty | 既定ルールの検知 |
|---|---|---|
kubectl exec -n fanclub <pod> -c backend -- sh -c 'id' | 0 | 検知されません |
kubectl exec -it -n fanclub <pod> -c backend -- sh -c 'id' | 34816 | Notice A shell was spawned in a container with an attached terminal |
同じことをしているのに、片方だけ記録に残ります。
攻撃者の側から見ると、これは「-it を付けなければ既定ルールには映らない」ということです。 そして攻撃者が対話端末を欲しがるとは限りません——スクリプトから 1 行ずつ実行するほうが、痕跡も残らず自動化もできます。
既定ルールは「運用者が手で入ってきた」を捉えるのに向いています。 それは監査の観点では有用ですが、攻撃の検知としては網が粗いところです。 だからカスタムルールを書きます——次の演習がそれです。
演習②のゴール確認
| 確認 | 期待 |
|---|---|
tty なしの exec | Falco のログに 1 件も出ません |
tty ありの exec | Notice A shell was spawned in a container with an attached terminal |
アラートの k8s_pod_name / k8s_ns_name | Pod 名と fanclub が入っています |
アラートの container_image_tag | 0.4.1 |
本番ガードレール⑦: 「検知されなかった」を「起きなかった」と読みません。
検知の仕組みを入れたら、必ず「検知されるはずの操作」を 1 度打って、アラートが出ることを確かめます。
確かめないまま運用に入ると、静かな状態が 2 通りの意味を持ってしまいます——何も起きていないのと、見ていないのです。この 2 つを区別できない監視は、監視として機能しません。
本番では、定期的に「検知されるはずの操作」を意図的に打つ(合成監視)ことも検討します。第17回のアラート連携と組み合わせると、「アラートが届く経路まで生きているか」を定期確認できます。
試験ではこう問われる。
「コンテナ内でシェルが起動したことを検知するルールが動作しているか確認せよ」という粒度で出ます。手が覚えているべきは、①アラートの見方(kubectl logs -n <ns> -l <label> -c falco)②検知を起こす操作(kubectl exec -it ... -- sh)の 2 つです。
falco.org/docs は参照できるので、ルールの書き方・フィールド名・既定マクロの説明は引けます。 ただし既定ルール 1 本 1 本の condition の全文は、公式ドキュメントではなくルールファイル本体にあります——その置き場所(github.com)は CKS では参照できません。
だから条件を確かめたいときは、実機を読むのが唯一の経路です——grep -A 12 'rule: <ルール名>' /etc/falco/falco_rules.yaml。この打ち方こそ手が覚えているべきものです。試験の SSH ホストには alias k=kubectl が設定済みです。

カスタムルールはどこに置かれ、いつ反映されるか —— rules.d/ と 2 つの経路
演習③に入る前に、ルールの置き場所と反映の経路を確認します。ここは世に出回っている手順と本ラボの実機が食い違うところなので、実測に置き換えて読みます。
よく書かれている手順と、本ラボの実機
| よく書かれている手順 | 本ラボの実機 |
|---|---|
カスタムルールは falco_rules.local.yaml に書く | chart の customRules は /etc/falco/rules.d/<キー名> に置かれます。falco_rules.local.yaml は rules_files に列挙されていますが、chart はこのファイルを作りません |
| ルール変更後は SIGHUP を送るか Pod を再起動する | watch_config_files: true が既定なので、ファイルが変わると自動でリロードされます(実測 20〜77 秒) |
どちらの手順も間違いではありません。 ホストに直接 Falco をインストールした構成(systemd で動かす形)では、/etc/falco/falco_rules.local.yaml を編集して systemctl restart falco あるいは kill -1 を送るのが定石です。Helm chart で DaemonSet として動かすと置き場所が変わります。
試験環境の Falco がどちらの構成かは、環境を見て判断します。 falco.org/docs は参照できるので、「どこに置くか」で迷ったら rules_files の設定を実機で確認するのが早い経路です——/etc/falco/falco.yaml の rules_files に列挙されたパスが答えです。
rules_files の読み込み順
本ラボの実測です。
rules_files:
- /etc/falco/falco_rules.yaml
- /etc/falco/falco_rules.local.yaml
- /etc/falco/rules.d
後から読まれたものが後勝ちです。 rules.d が最後なので、ここに置いたルールが既定を上書きできます。 演習④で使う override は、この順序があって初めて成立します。
既定の falco_rules.yaml を直接編集しないでください。 falcoctl がルールを更新したときに上書きされて消えるからです。編集していい場所は falco_rules.local.yaml と rules.d/ の 2 つだけです。
chart の customRules が置かれる場所
values.yaml に次のように書くと、rules.d/ の下にキー名のファイルが作られます。
customRules:
rules-fanclub.yaml: |-
- list: fanclub_shell_binaries
items: [sh, ash, bash, busybox]
Pod の中での実体です(実測)。
/etc/falco/rules.d/rules-fanclub.yaml -> ..data/rules-fanclub.yaml
rules.d/ の中身は ConfigMap のマウントなので、実体は symlink です。 ConfigMap の内容が変わると、kubelet が ..data の指す先を差し替えます。
この差し替えで inotify のイベントが複数回発火するため、ConfigMap を直接更新したときは、Falco のリロードログが 2 回出ます(後述します)。「2 回出ているから壊れている」ではありません。 なお helm upgrade 経由(Pod が作り直される経路)では 1 回だけです。
反映の経路が 2 つある
| 経路 | 何が起きるか | 実測 |
|---|---|---|
helm upgrade -f values.yaml | ConfigMap のチェックサム annotation が変わり、DaemonSet がローリング再起動します | 5 台で約 2 分(実測 122 秒 / 126 秒) |
ConfigMap を直接 kubectl apply(または kubectl edit) | Pod はそのままです。ファイルが反映されると Falco が自動リロードします | 20 / 31 / 41 / 51 / 77 / 87 秒(通算 6 回の計測ですべて違いました) |
同じ「ルールを更新する」でも、経路によってクラスタへの影響が違います。
helm upgrade は DaemonSet を作り直します。 5 台のノードで Falco が順番に落ちて上がるので、その間そのノードは監視されていません。 本ラボでは約 2 分ですが、ノードが多いほど長くなります。
ConfigMap の直接更新は Pod を落としません。 20 秒から 77 秒で反映されます。ただし Helm のリリースとは食い違います——次に helm upgrade を打つと、ConfigMap が values.yaml の内容で上書きされて、直接編集した分は消えます。
本番では「Helm の values を正とし、helm upgrade で配る」が原則です。 直接編集は実験と緊急対応のための手段であり、変更を残すなら values.yaml へ書き戻します。
反映時間は一定しない
実測(通算 6 回計測): 20 / 31 / 41 / 51 / 77 / 87 秒。 6 回ともすべて違う値になりました。kubelet が ConfigMap をノードへ同期する周期に当たるタイミング次第なので、一定しません。
本文では「1 分前後・最大 2 分弱」と見込みます。 「◯秒で反映される」と断定すると、30 秒待って「反映されない」と判断して別の手を打つことになります。第14回の denyTTL と同じで、反映を待つ時間を知らないと切り分けができません。
ConfigMap を直接更新したときは、リロードのログが 2 回出る
実行コマンド(k8s-ops・developer):
$ kubectl logs -n falco -l app.kubernetes.io/name=falco -c falco --tail=30 --prefix | grep -E 'Loading rules|schema validation'
実行結果(抜粋):
Sat Aug 01 13:50:37 2026: Loading rules from:
Sat Aug 01 13:50:37 2026: /etc/falco/falco_rules.yaml | schema validation: ok
Sat Aug 01 13:50:37 2026: /etc/falco/rules.d/rules-fanclub.yaml | schema validation: ok
Sat Aug 01 13:50:38 2026: Loading rules from:
Sat Aug 01 13:50:38 2026: /etc/falco/falco_rules.yaml | schema validation: ok
Sat Aug 01 13:50:38 2026: /etc/falco/rules.d/rules-fanclub.yaml | schema validation: ok
同じ 3 行が 1 秒差で 2 回出ます。 ConfigMap の symlink 差し替えで inotify イベントが複数発火するためです。壊れているわけでも、ルールが 2 回読まれて重複するわけでもありません。
見るべきは schema validation: ok の側です。 ルールに構文誤りがあると、ここが ok にならず、エラーの内容が続けて出ます。 リロードの回数ではなく、検証結果を読みます。
本番ガードレール⑧: ルールを配ったら、「配られたこと」ではなく「読み込まれたこと」を確認します。
ConfigMap を更新した時点では、まだどのノードにも反映されていません。
確認するのは 3 点です——①schema validation: ok が出ているか ②5 台すべてで出ているか ③検知されるはずの操作を打って実際にアラートが出るか。 ①だけで満足すると、「構文は正しいが 1 件も当たらないルール」を配ったことに気づけません。
試験ではこう問われる。
「指定のルールファイルを配置し、Falco に読み込ませよ」という粒度で出ます。手が覚えているべきは、①ルールの置き場所を rules_files から調べる ②反映のさせ方(自動リロードか再起動か)を確認する、の 2 つです。
試験環境が systemd 構成なら systemctl restart falco、DaemonSet 構成なら ConfigMap の更新か kubectl rollout restart ds/<name> -n <ns> になります。 falco.org/docs は参照できるので、watch_config_files の既定値も引けます。
やってみよう③:カスタムルールを書き、sidecar が漏れることを見つける
所要時間の目安: 約 12 分(反映待ちを含みます)。list / macro / rule を書き、ConfigMap 経由で配り、検知を確認し、漏れを見つけて直します。試験相当の作業(ルールを書いて反映させて検知させる)なら 6 分です。
ステップ1:ルールを書く
次の 3 要素をまとめて 1 ファイルに書きます。
- list: fanclub_shell_binaries
items: [sh, ash, bash, busybox]
- macro: fanclub_backend_container
condition: (container.image.repository endswith "fanclub-backend")
- rule: Shell spawned in fanclub-backend
desc: fanclub-backend コンテナ内でシェルが起動した
condition: >
spawned_process
and container
and fanclub_backend_container
and proc.name in (fanclub_shell_binaries)
output: >
fanclub-backend でシェル起動 (user=%user.name uid=%user.uid proc=%proc.name
exe=%proc.exepath cmd=%proc.cmdline tty=%proc.tty
image=%container.image.repository:%container.image.tag
pod=%k8s.pod.name ns=%k8s.ns.name)
priority: WARNING
tags: [fanclub, shell, cks]
3 要素の役割です。
list: プロセス名の集合です。shだけでなくash/bash/busyboxも入れているのは、Alpine ではshの実体が busybox で、呼び方によってプロセス名が変わりえるからですmacro: 条件の部品です。container.image.repository endswith "fanclub-backend"を名前で呼べるようにしておくと、同じ条件を複数のルールで使い回せますrule: 既定のマクロspawned_process(プロセスが起動した)とcontainer(コンテナ内である)を組み合わせます。 既定ルールが定義しているマクロは、カスタムルールからそのまま呼べます
condition に proc.tty の条件を入れていません。 だからtty の有無にかかわらず検知します——これが既定ルールとの違いです。
% で始まるのがフィールドの参照です。 出力に何を入れるかは自由に決められますが、後で読む人が判断できる情報を入れます。
本ルールでは tty=%proc.tty を入れています。 演習②で「tty があるかどうかで既定ルールの発火が変わる」ことを確認したので、カスタムルールの出力にも tty を出して、対比できるようにしています。
日本語の output もそのまま出力されます(実測)。本番で日本語を使うかは、アラートを受け取る先(第17回の連携先)が扱えるかで決めます。
ステップ2:values.yaml に書いて配る
ファイル(k8s-ops 上・~/falco-lab/custom-values.yaml):
customRules:
rules-fanclub.yaml: |-
- list: fanclub_shell_binaries
items: [sh, ash, bash, busybox]
- macro: fanclub_backend_container
condition: (container.image.repository endswith "fanclub-backend")
- rule: Shell spawned in fanclub-backend
desc: fanclub-backend コンテナ内でシェルが起動した
condition: >
spawned_process
and container
and fanclub_backend_container
and proc.name in (fanclub_shell_binaries)
output: >
fanclub-backend でシェル起動 (user=%user.name uid=%user.uid proc=%proc.name
exe=%proc.exepath cmd=%proc.cmdline tty=%proc.tty
image=%container.image.repository:%container.image.tag
pod=%k8s.pod.name ns=%k8s.ns.name)
priority: WARNING
tags: [fanclub, shell, cks]
実行コマンド(k8s-ops・developer):
$ helm upgrade falco oci://ghcr.io/falcosecurity/charts/falco --version 9.1.0 --namespace falco -f ~/falco-lab/custom-values.yaml
実測値です。
| 項目 | 実測 |
|---|---|
helm upgrade 後の DaemonSet ローリング再起動 | 5 台で約 2 分(実測 122 秒 / 126 秒) |
helm upgrade を選ぶと DaemonSet が作り直されます。 5 台が順番に落ちて上がる間、そのノードは監視されていません。 ルールの追加のたびに 2 分の空白を作るのは、本番では避けたいところです。
だからステップ 7 で、Pod を落とさない経路(ConfigMap の直接更新)も試します。 本演習では両方を確認します。
ステップ3:反映を確認する
実行コマンド(k8s-ops・developer):
$ kubectl exec -n falco <falco Pod 名> -c falco -- ls -l /etc/falco/rules.d/
実行結果:
rules-fanclub.yaml -> ..data/rules-fanclub.yaml
ログでも確認します。
実行コマンド(k8s-ops・developer):
$ kubectl logs -n falco <falco Pod 名> -c falco --tail=20 | grep -E 'Loading rules|schema validation'
実行結果:
Loading rules from:
/etc/falco/falco_rules.yaml | schema validation: ok
/etc/falco/rules.d/rules-fanclub.yaml | schema validation: ok
ステップ4:tty なしで検知されることを確認する
実行コマンド(k8s-ops・developer):
$ kubectl exec -n fanclub <backend Pod 名> -c backend -- sh -c 'echo from-backend'
実行結果:
from-backend
Falco のログ(実測):
16:15:29.316042941: Warning fanclub-backend でシェル起動 (user=<NA> uid=10001 proc=sh
exe=/bin/busybox cmd=sh -c echo from-backend tty=0
image=k8s-registry:5000/fanclub-backend:0.4.1 pod=fanclub-backend-56875d65c9-m5q8c ns=fanclub)
tty=0 と出ています。 演習②で既定ルールが見逃した打ち方が、カスタムルールでは検知されています。
priority も Warning になっています(既定ルールは Notice でした)。自分で書いたルールは、自分で重要度を決められます。
user=<NA> になっているのは異常ではありません。
演習②の既定ルールでは user=appuser と出ていました。同じコンテナの同じ uid でも、名前が出るときと出ないときがあります(実測)。uid からユーザー名への変換はコンテナ内の /etc/passwd を読めるかどうかに依存するためです。
だから output には %user.uid を必ず併記します。 アラートを見る人が知りたいのは「誰が」であり、名前が出ないときに数字すら残らないのでは調べようがありません。 %user.name だけに頼らないでください。
exe=/bin/busybox も出ています。 proc.name は sh ですが、実体は busybox です。 proc.name だけで判定するルールは、別名で呼ばれると外れます——output に proc.exepath を入れておくと、後から実体を確認できます。
ステップ5:同じ Pod の sidecar で試す
backend Pod の READY は 2/2 です。動き続けているコンテナが 2 つ、起動時だけ動く init コンテナが 1 つという内訳になっています。
| コンテナ | 種別 | READY に数える | イメージ | 起動コマンド |
|---|---|---|---|---|
wait-for-db | init(起動時だけ動きます) | 数えません | busybox:1.37 | — |
log-shipper | native sidecar(restartPolicy: Always を持つ init コンテナ。本体より先に起動し、本体と共に生き続けます) | 数えます | busybox:1.37 | sh -c "echo sidecar; tail -F /var/log/app/app.log 2>/dev/null || sleep infinity" |
backend | 本体 | 数えます | fanclub-backend:0.4.1 | — |
実行コマンド(k8s-ops・developer):
$ kubectl exec -n fanclub <backend Pod 名> -c log-shipper -- sh -c 'echo from-sidecar'
実行結果:
from-sidecar
Falco のログ:
(1 件も出ません)
| 実行先 | カスタムルール(image 版) |
|---|---|
kubectl exec … -c backend -- sh -c 'echo from-backend' | 検知されます |
kubectl exec … -c log-shipper -- sh -c 'echo from-sidecar' | 検知されません |
同じ Pod の中で、同じ sh を起動しています。それでも片方は検知されません。
原因はルールの macro にあります——container.image.repository endswith "fanclub-backend" で絞っているので、イメージが busybox:1.37 の sidecar は条件に当たりません。
「fanclub-backend を監視するルールを書いた」つもりでしたが、実際に監視しているのは「fanclub-backend というイメージのコンテナ」です。 Pod とコンテナは別物であり、1 つの Pod の中に複数のイメージが同居します。
そして sidecar は攻撃者にとって都合がよい存在です。 busybox には sh も wget も nc も入っており、アプリケーション本体より道具が揃っています。 第12回で「最小イメージにする」を扱いましたが、同じ Pod に busybox の sidecar が居るなら、本体を最小化した効果は薄くなります。
同じ理由で ephemeral container(kubectl debug で差し込むコンテナ)も漏れます——次の節で扱います。

ステップ6:k8s.pod.name で絞る版へ直す
rules-fanclub.yaml の最終形を全量で示します。image 版のマクロとルールは削除して置き換えます——併存させません。
- list: fanclub_shell_binaries
items: [sh, ash, bash, busybox]
- macro: fanclub_backend_pod
condition: (k8s.ns.name = "fanclub" and k8s.pod.name startswith "fanclub-backend")
- rule: Shell spawned in fanclub-backend pod
desc: fanclub-backend Pod のいずれかのコンテナ内でシェルが起動した
condition: >
spawned_process
and container
and fanclub_backend_pod
and proc.name in (fanclub_shell_binaries)
output: >
fanclub-backend Pod でシェル起動 (user=%user.name uid=%user.uid proc=%proc.name
exe=%proc.exepath cmd=%proc.cmdline tty=%proc.tty
container=%container.name
image=%container.image.repository:%container.image.tag
pod=%k8s.pod.name ns=%k8s.ns.name)
priority: WARNING
tags: [fanclub, shell, cks]
2 本を併存させると、どちらが発火したのか読めなくなります。 前節で見たとおり falco.yaml の rule_matching は first(最初にマッチしたルールで打ち切る)なので、backend コンテナでは先に読まれたほうだけが出ます。 「広げたつもりが、片方しか動いていない」という状態は、本回がずっと扱っている「静かなまま気づかない」そのものです。
絞り込みを直すときは、置き換えます。 足すのではありません。
output に %container.name を足しています。 Pod で絞ると、どのコンテナで起きたかが分からなくなるからです。backend なのか log-shipper なのか、アラートを見た人が判断できるようにします。
絞り方を広げたら、出力に情報を足します。 「検知範囲を広げる」と「アラートが読める」は別の作業です。
%user.name だけに頼らないでください。
同じコンテナの同じ uid でも、%user.name が <NA> になることがあります(実機で確認しました)。uid からユーザー名への解決は、コンテナの /etc/passwd を読めるかどうかに依存するためです。%user.uid を必ず併記します。 アラートを見る人が知りたいのは「誰が」であり、名前が出ないときに数字すら残らないのは困ります。
あわせて %proc.exepath はコンテナによって違う値を返します。 backend(Alpine)で sh を打つと /bin/busybox、log-shipper(busybox イメージ)だと /bin/sh です。proc.name は両方 sh なので、ルールを proc.exepath で書くと取りこぼします。
実機検証済みです。 k8s.pod.name startswith "fanclub-backend" は backend コンテナと sidecar log-shipper の両方を検知し、fanclub-frontend-… Pod は通算 0 件で巻き込まないことを確認しました(累計ヒット数の差分で測定しています)。
ステップ7:ConfigMap を直接更新して反映させる(Pod を落とさない経路)
実行コマンド(k8s-ops・developer):
$ kubectl get cm -n falco
$ kubectl edit configmap -n falco falco-rules
実測値です。
| 項目 | 実測 |
|---|---|
| ファイル反映までの時間 | 20 秒 / 51 秒 / 77 秒(通算 6 回の計測ですべて違いました) |
| Pod の再起動 | なし(RESTARTS は増えません) |
| リロードのログ | 同じ内容が 2 回出ます |
RESTARTS が増えていないことを確認します。 Pod を落とさずにルールが入れ替わったことが、この 1 列で分かります。
反映は 1 分前後・最大 2 分弱を見込みます。 30 秒待って出ないからといって、別の手を打たないでください。
秒数そのものは覚えなくて構いません。 これは kubelet が ConfigMap をノードへ同期する周期に当たるかどうかで決まる値で、本書ラボの通算 6 回の計測でも 20・31・41・51・77・87 秒とすべて違いました。 覚えるのは「ConfigMap を更新すれば Pod を落とさずに反映される」という性質のほうです。
ステップ8:両方のコンテナで検知されることを確認する
実行コマンド(k8s-ops・developer):
$ kubectl exec -n fanclub <backend Pod 名> -c backend -- sh -c 'echo from-backend'
$ kubectl exec -n fanclub <backend Pod 名> -c log-shipper -- sh -c 'echo from-sidecar'
実行結果(Falco のログ):
Warning fanclub-backend Pod でシェル起動 (... container=backend image=k8s-registry:5000/fanclub-backend:0.4.1 ...)
Warning fanclub-backend Pod でシェル起動 (... container=log-shipper image=docker.io/library/busybox:1.37 ...)
2 件とも検知されます。 container= の値が違うので、同じ Pod のどちらのコンテナで起きたかが読めます。
演習③のゴール確認
| 確認 | 期待 |
|---|---|
rules.d/rules-fanclub.yaml が 5 台に存在 | symlink で存在します |
schema validation | ok(5 台とも) |
tty なしの exec(backend) | Warning fanclub-backend Pod でシェル起動 ... tty=0 |
tty なしの exec(log-shipper) | image 版では検知されず、pod 版では検知されます |
Pod の RESTARTS | ConfigMap 直接更新では増えません |
| 既存の 111 Pod | 異常 0 のまま |
本番ガードレール⑨: ルールの絞り込みは、絞った瞬間に「見なくなるもの」を作ります。
書いたら必ず「当たらないはずのもの」も 1 度打って、当たらないことを確認します。
絞り方の選択肢は 3 つあります——①イメージで絞る(container.image.repository)②Pod で絞る(k8s.pod.name / k8s.ns.name)③コンテナ名で絞る(container.name)。目的によって選びます。
| 絞り方 | 見えるもの | 見えないもの |
|---|---|---|
| イメージ | そのイメージのコンテナ | 同じ Pod の別イメージ(sidecar / ephemeral container) |
| Pod | Pod 内のすべてのコンテナ | 別の Pod(名前が変わると外れます) |
| namespace | namespace 内のすべて | 誤検知が増えます |
「監視したい単位」を決めてから、フィールドを選びます。 フィールドを先に選ぶと、書いた本人も何を監視しているか説明できなくなります。
試験ではこう問われる。
「指定のコンテナ / Pod で指定の挙動が起きたときに検知するルールを作り、指定のファイルへ出力せよ」という粒度で出ます。手が覚えているべきは、①rule / macro / list の 3 要素のキー名 ②condition に spawned_process and container を書く型 ③output の %フィールド名 記法です。
falco.org/docs は参照できるので、フィールド名と既定マクロの一覧は引けます。 ただし引く時間が惜しいので、spawned_process / container / proc.name / proc.cmdline / container.image.repository / k8s.pod.name / k8s.ns.name くらいは覚えておきます。
出力先を指定される設問では、Falco のログをリダイレクトするのではなく、falco.yaml の file_output を使う書き方も知っておきます。
file_output:
enabled: true
filename: /opt/course/xx/alerts
kubectl debug は PSA restricted の namespace で 3 段階に詰まる
この節は演習ではありません(手を動かすかは読者に任せます)。演習③で見つけた「同じ Pod の別イメージは漏れる」の続きであり、同時に第8回の回収でもあります。
なぜこの節を置くか
- 第18回の DAIR で「証拠保全」の手段として ephemeral container を使います。 そのときに詰まらないよう、先に詰まり方を確認しておきます
- ephemeral container は演習③の image 版ルールから漏れます。 差し込んだコンテナは別イメージなので、イメージで絞ったルールには当たりません——攻撃者が使えば、同じ理由で見えません
- 第8回で
fanclubnamespace に PSAenforce: restrictedを適用しました。 その制約が運用者自身のデバッグ手段にもそのまま効くことを、実機で確認します
先に断っておきます。
--custom まで要るのは本書ラボ固有の組み合わせによります——fanclub が PSA enforce: restricted で、かつ差し込む busybox が root で動くイメージだからです。
試験で覚える価値があるのは kubectl debug に --profile があるところまでです。--custom の JSON を暗記する必要はありません。 ただし「デバッグ用のコンテナも Pod の一員なので、その namespace の制約を受ける」という理屈は、試験でも現場でも同じように効きます。
ephemeral container とは、kubectl debug で稼働中の Pod へ後から差し込む一時的なコンテナです。Pod を作り直さずに調査できる代わりに、一度差し込むと削除できません(後述します)。
3 段階の詰まり方
| # | 打ち方 | 結果 |
|---|---|---|
| 1 | kubectl debug <pod> --image=busybox:1.37 --target=backend(既定 = legacy プロファイル) | Error from server (Forbidden)。allowPrivilegeEscalation != false / unrestricted capabilities / runAsNonRoot != true の 3 点で PSA 違反。--profile=legacy is deprecated の警告も出ます |
| 2 | --profile=restricted を追加 | PSA は通りますが CreateContainerConfigError。container has runAsNonRoot and image will run as root |
| 3 | --profile=restricted --custom=<file>(runAsUser: 10001) | 起動して終了(exitCode: 1・ls が失敗するため)(uid=10001 gid=0(root)) |
段階 1: 既定のまま打つと Forbidden
実行コマンド(k8s-ops・developer):
$ kubectl debug -n fanclub <backend Pod 名> --image=busybox:1.37 --target=backend -- sh -c 'id'
実行結果(要点):
Warning: --profile=legacy is deprecated
Error from server (Forbidden): pods "..." is forbidden: violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true
kubectl debug の既定プロファイルは legacy で、セキュリティ制約を何も付けません。 第8回で fanclub namespace に enforce: restricted を付けたので、そのままでは admission で弾かれます。
違反は 3 点です——allowPrivilegeEscalation != false / unrestricted capabilities / runAsNonRoot != true。restricted プロファイルの要件を、kubectl debug が 1 つも満たしていません。
--profile=legacy is deprecated の警告も出ます。 既定が非推奨のまま残っているので、「既定で打つ」こと自体が推奨されていません。
段階 2: --profile=restricted を足すと今度はイメージ側で詰まる
実行コマンド(k8s-ops・developer):
$ kubectl debug -n fanclub <backend Pod 名> --image=busybox:1.37 --target=backend --profile=restricted -- sh -c 'id'
実行結果(要点):
CreateContainerConfigError
container has runAsNonRoot and image will run as root
admission は通りましたが、kubelet がコンテナを起動できません。
--profile=restricted は runAsNonRoot: true を付けますが、runAsUser は付けません。 一方 busybox:1.37 は Dockerfile に USER の指定が無く、root で動きます。 「root で動くな」と指示されたのに「誰で動け」が指定されていないので、kubelet は判断できずに失敗します。
これは第8回で扱った restricted の要件そのものです。 runAsNonRoot: true を満たすには、イメージ側に非 root の USER があるか、Pod 側に runAsUser があるかのどちらかが要ります。プロファイルは前者を仮定しています。
拒否される場所が 2 段階に分かれていることも重要です——1 段階目は API サーバの admission、2 段階目はノードの kubeletです。第14回の ImagePolicyWebhook も admission でしたが、admission を通ったからといって起動するとは限りません。
段階 3: --custom で runAsUser を渡すと起動する
ファイル(k8s-ops 上・~/falco-lab/debug-profile.json):
{
"securityContext": {
"runAsUser": 10001,
"runAsNonRoot": true,
"allowPrivilegeEscalation": false,
"capabilities": { "drop": ["ALL"] },
"seccompProfile": { "type": "RuntimeDefault" }
}
}
実行コマンド(k8s-ops・developer):
$ kubectl debug -n fanclub <backend Pod 名> --image=busybox:1.37 --target=backend --profile=restricted --custom=/home/developer/falco-lab/debug-profile.json -- sh -c 'id'
実行結果:
uid=10001 gid=0(root)
--custom には絶対パスを渡しています。 bash のチルダ展開は --custom=~/… の形では働かないことがあるためで、ここで詰まると原因が分かりにくくなります。
runAsUser: 10001 を指定して起動しました。 第6回・第8回で backend に設定した uid と同じ値です。同じ uid にすることで、対象コンテナのファイルにアクセスできる可能性が上がります——ただし次のとおり、それでも限界があります。
JSON の 5 項目は restricted プロファイルの要件そのものです。--profile=restricted が付ける分と重複していますが、--custom は上書きなので、必要な項目は明示的に並べます。
--target を付けても /proc/1/root/ は読めない
実行コマンド(起動した ephemeral container の中):
$ ls /proc/1/root/
実行結果:
ls: /proc/1/root/: Permission denied
--target=backend を付けると PID 名前空間を共有するので、対象コンテナのプロセスが /proc に見えます。 それでも /proc/1/root/ は読めません。
理由は uid です。 ephemeral container は runAsUser: 10001 で動いていますが、/proc/1/root は root 所有なので、10001 では辿れません。
「PID 名前空間を共有すれば対象コンテナのファイルが見える」は成り立ちません。 見えるのはプロセスの一覧までで、ファイルシステムを辿るには権限が要ります。
restricted の制約を守りながら調べられる範囲は、思ったより狭いところです。 第18回の証拠保全では、この制約を前提に手段を選びます——kubectl cp・ログ・Falco のアラートそのものが、実質の証拠になります。
失敗した ephemeral container は削除できない
実測では、失敗した ephemeral container は CreateContainerConfigError のまま Pod に残り、削除できませんでした。
ephemeral container は追加専用です。 1 度差し込んだら、Pod を作り直すまで消えません。
段階 1 と段階 2 で失敗した分が、そのまま Pod にぶら下がります。 kubectl get pod -o yaml の ephemeralContainers に残り、kubectl describe にもエラーのまま出続けます。
やり直すときは、別の Pod で試すか、Pod を作り直します。 本番の Pod で試行錯誤すると、そのオブジェクトが「エラーを抱えた Pod」として監視に映り続けます——調査のつもりが、別のアラートの原因になります。
プロファイルは打つ前に決めます。 --profile=restricted --custom=<file> の形をあらかじめ用意しておくのが実務の作法です。
ephemeral container は image で絞ったルールから漏れる
ephemeral container のイメージは busybox:1.37 です。 演習③のステップ 1〜4 で書いた image で絞るルールには当たりません。sidecar が漏れたのとまったく同じ理由です。
ステップ 6 で k8s.pod.name 版へ直した後なら、ephemeral container も検知範囲に入ります——実測で確認できます。
実行結果(pod 版のルールが入った状態で kubectl debug を打ったときの Falco のログ・実測):
21:37:54.414180479: Warning fanclub-backend Pod でシェル起動 (user=<NA> uid=10001 proc=sh
exe=/bin/sh cmd=sh -c echo eph-check tty=0 container=debugger-4bhjz
image=docker.io/library/busybox:1.37 pod=fanclub-backend-56875d65c9-6cdm7 ns=fanclub)
container=debugger-4bhjz / image=docker.io/library/busybox:1.37 と出ています。 差し込んだコンテナの名前とイメージがそのまま記録に残ります——Pod で絞っておけば、後から差し込まれたものも同じ網に入るということです。
これは攻撃側から見ると分かりやすい抜け道です。 kubectl debug を打てる権限があれば、監視対象のイメージ以外のコンテナを、監視対象の Pod の中に差し込めます。 第4回で RBAC を絞ったのは、こういう操作を打てる主体を減らすためでもあります。
そして「誰が kubectl debug を打ったか」は Falco には出ません——第16回の監査ログの担当です(pods/ephemeralcontainers への操作として記録されます)。
3 段階のまとめ
| 段階 | 追加したもの | 止まった場所 | 原因 |
|---|---|---|---|
| 1 | (なし・legacy) | API サーバの admission | PSA restricted の 3 要件を満たしていません |
| 2 | --profile=restricted | ノードの kubelet | runAsNonRoot: true はありますが runAsUser が無く、イメージが root で動きます |
| 3 | --custom(runAsUser: 10001) | 起動しました | — |
| — | — | /proc/1/root/ は Permission denied | uid 10001 では root 所有のパスを辿れません |
本番ガードレール⑩: 調査手段は、事故が起きる前に試しておきます。
PSA restricted を適用した namespace では、kubectl debug がそのままでは通りません。
事故の最中にプロファイルの試行錯誤をすると、失敗した ephemeral container が Pod に残り続けます。 --profile=restricted --custom=<file> の形をあらかじめ用意し、検証環境で 1 度通しておきます。
加えて、調査用の最小イメージを決めておきます。 busybox は root で動くので runAsUser の指定が要ります。非 root で動く調査用イメージを用意しておけば、--profile=restricted だけで通ります。
試験ではこう問われる。
kubectl debug そのものは CKS でも CKA でも問われえます。 「指定の Pod を調査するために ephemeral container を差し込め」という粒度です。手が覚えているべきは kubectl debug <pod> -it --image=<img> --target=<container> の型です。
--profile の存在は覚えておく価値があります。 試験の namespace が restricted とは限りませんが、restricted なら既定では通りません。 kubernetes.io/docs は参照できるので、--profile の値(legacy / general / baseline / restricted / netadmin / sysadmin)は引けます。
--custom まで要求される設問は考えにくいところです——ラボ固有の事情(busybox が root で動く / backend の uid が 10001)に依存するためです。
やってみよう④:誤検知を再現してから潰す
所要時間の目安: 約 12 分(override の反映 31 秒と、ConfigMap の削除 → helm upgrade による書き戻しを含みます。書き戻しではローリング再起動は起きません)。誤検知を意図的に発生させ、override で潰し、他の検知が生き残っていることを 3 通りで確認します。 試験相当の作業(例外マクロを override して反映させる)なら 4 分です。
先に断っておきます。
この演習の後半(values.yaml への書き戻しと、そこで起きる Helm の衝突)は本書ラボの事情です。 試験では Falco が Helm で入っているとは限らず、helm.sh/docs は CKS では参照できません——Helm の運用そのものが出題範囲外です。
試験相当の作業は、前半の「例外マクロを override して反映させる」だけで 4 分です。それでも後半を通す理由は、「成功と表示されたものが成功しているとは限らない」という読み方が、試験でも現場でも同じように効くからです。
誤検知は「待つ」のではなく「起こす」
誤検知のチューニングを演習にするとき、いちばんやりがちな失敗は「発火するのを待つ」ことです。
待って測ると、次のことが起きます——①適用前の 3 分間がもともと 0 件だった ②override を適用した ③適用後の 3 分間も 0 件だった。 これでは「効いた」証明になりません。 もともと 0 件だったものが 0 件のままなだけです。
第13回・第14回で繰り返してきた「測りたいものを測る」の 3 例目です。 本演習は「発火させてから測る」に切り替えます。
題材選びで 2 回失敗しました
「発火させてから測る」に切り替えても、まだ足りませんでした。 本書ラボに実在する誤検知を 2 つ調べましたが、どちらも演習には使えませんでした。 使えなかった理由のほうが、Falco の理解に効きます。
| ルール | 発火元 | 実測 | 演習で使えるか |
|---|---|---|---|
Redirect STDOUT/STDIN to Network Connection in Container | longhorn-manager(longhorn backup cleanup-all-mounts) | k8s-wl-01 のみ 20 分で 6 件(2 つの時刻に各 3 件)。以降は止まります | 使えません(間欠的で、起こしたいときに起こせません) |
Contact K8S API Server From Container | fluent-bit(docker.io/grafana/fluent-bit-plugin-loki・monitoring ns) | Pod を消しても出たり出なかったりします(下記) | 使えません |
fluent-bit のほうは、一度は「Pod を消せば必ず出る」ように見えました。 実際、最初の観察では 0 件 → Pod 削除 → 1 件と再現しました。ところが日を改めて同じ手順を踏むと、1 件も出ませんでした。
| 試したこと | 結果 |
|---|---|
| k8s-wl-01 の fluent-bit Pod を削除 → 80 秒 | 0 件 |
| 5 台すべての Falco の通算件数 | 0 件 |
| k8s-cp-01 の fluent-bit Pod を削除 → 90 秒 | 1 件(Control Plane では出ました) |
fluent-bit の DaemonSet を rollout restart(5 台全部) | 0 件(今度は Control Plane でも出ません) |
理由は、ルールの定義を読めば分かります。
実行コマンド(k8s-ops・developer):
$ kubectl exec -n falco <falco Pod 名> -c falco -- grep -A 2 'macro: k8s_api_server' /etc/falco/falco_rules.yaml
実行結果:
- macro: k8s_api_server
condition: (fd.sip.name="kubernetes.default.svc.cluster.local")
fd.sip.name は「接続先 IP の逆引き名」です。 つまりこのルールは、Falco が 10.96.0.1 を kubernetes.default.svc.cluster.local という名前として解決できたときにだけ発火します。
接続そのものは毎回起きています。 起きていないのは名前の解決のほうで、これはノードによっても、タイミングによっても変わります。 本書ラボでは Control Plane では解決できることがあり、Workload ノードでは通算 0 件でした。
ここで得られる教訓は 2 つあります。
1 つ目は、ルールの条件は「何を見ているか」まで読むということです。ルール名は Contact K8S API Server From Container——「コンテナから API サーバへの接続」です。名前からは「接続したかどうか」を見ているように読めます。 実際に見ているのは「接続先の逆引き名」で、名前が引けなければ、接続しても検知しません。
2 つ目は、「1 回出た」を「いつでも出る」と読み替えないということです。最初の観察では確かに再現しました。1 回の成功を一般化したことが誤りでした。 再現性は「条件を変えて何度も出る」で確かめます。
そこで、本演習は読者が確実に再現できる題材に変えます——演習②で自分の手で発生させた Terminal shell in container です。
これは誤検知として理想的な題材です。 運用者が調査のために kubectl exec -it を打つたびにアラートが出る——攻撃とは何の関係もない、日常の運用操作が検知されるという、いちばんありふれた形だからです。
チューニング用の「空マクロ」という仕組み
既定ルールの多くは and not user_◯◯_conditions(または user_known_◯◯_activities)という条件を持ち、そのマクロの既定値は (never_true) になっています。
| ルール | チューニング用マクロ |
|---|---|
Terminal shell in container | user_expected_terminal_shell_in_container_conditions |
Contact K8S API Server From Container | user_known_contact_k8s_api_server_activities |
Redirect STDOUT/STDIN to Network Connection in Container | user_known_stand_streams_redirect_activities |
既定は (never_true) なので、常に偽です。 つまり and not (never_true) は and true と同じで、何も除外していません。
ここを自分の環境に合わせた条件で置き換えることで、除外リストを作ります。 既定ルールの condition を直接書き換えないでください——falcoctl がルールを更新したときに消えますし、元のルールが何を検知していたかが分からなくなります。
replace は既定の条件を捨てる、append は足す
override には replace と append の 2 つがあります。 違いは既定の条件を残すかどうかです。
| 指定 | 何が起きるか | 既定が (never_true) の場合 |
|---|---|---|
condition: replace | 同名マクロの既定条件を丸ごと捨てて、書いた条件だけにします | 捨てるものが「何も除外しない」なので失うものはありません |
condition: append | 既定の条件に、書いた条件を足します(既定は壊しません) | (never_true) or <自分の条件> となり、結果は replace と同じです |
本回で置き換える 2 つのマクロは既定が (never_true) なので、どちらでも同じ結果になります。 それでも replace を選ぶのは、「置き換えるつもりで書いている」と読み手に伝えるためです。
危険なのは、既定が (never_true) でないマクロに replace を当てたときです。
チューニング用の空マクロではなく、実質的な絞り込み条件を持つマクロ(既定ルールが検知範囲を決めるために使っているもの)を置き換えると、その条件ごと消えます。
そのマクロを参照している既定ルールすべての挙動が、同時に変わります。 しかもエラーは出ません——schema validation: ok のまま、静かに検知範囲が変わります。 本回で何度も見てきた形です。
置き換える前に、そのマクロの既定の中身を必ず読んでください。 読み方は grep -A 3 'macro: <名前>' /etc/falco/falco_rules.yaml です。(never_true) だと確認できたものだけを replace の対象にします。
Redirect STDOUT/STDIN to Network Connection in Container の desc には、ルール自身が「このルールはノイズを出す。user_known_stand_streams_redirect_activities テンプレートマクロで調整できる」と書いてあります(実測)。ルールの作者が、チューニングされる前提で書いています。
「例外を書く場所があらかじめ用意されている」というのが Falco の設計です。 チューニングは想定された使い方であり、ルールをコピーして書き換えるのは最後の手段です。
ステップ1:baseline を測る
件数は「直近◯分」ではなく通算で数えます。 --since で窓を切ると、直前の操作で出たアラートが窓に入り込み、増分を読み違えます(この誤りは本書の検証でも起きました)。操作の前後で通算を数えて、その差を見ます。
実行コマンド(k8s-ops・developer):
$ kubectl logs -n falco <k8s-wl-01 上の falco Pod 名> -c falco | grep -c 'attached terminal'
実行結果(実測):
0
この数を控えておきます。 Pod を作り直していれば 0 ですが、演習②で -it を打っていれば 1 以上になっています。0 でなくても構いません——見るのは差だからです。
ステップ2:誤検知を起こす
運用者が調査のために入る、という想定です。 3 回打ちます。
実行コマンド(k8s-ops・developer):
$ kubectl exec -it -n fanclub <backend Pod 名> -c backend -- sh -c 'echo run-1'
$ kubectl exec -it -n fanclub <backend Pod 名> -c backend -- sh -c 'echo run-2'
$ kubectl exec -it -n fanclub <backend Pod 名> -c backend -- sh -c 'echo run-3'
ステップ3:数える
実行コマンド(k8s-ops・developer):
$ kubectl logs -n falco <k8s-wl-01 上の falco Pod 名> -c falco | grep -c 'attached terminal'
実行結果(実測):
3
3 回打って 3 件です。 打った回数と検知の件数が一致します——これが「起こしてから測る」の意味です。読者の環境でも、何度でも同じように再現します。
ルールが間違っているわけではありません。 「コンテナ内で対話端末付きのシェルが起動した」は侵入後の挙動としてよくある形であり、検知する価値があります。問題は、正規の運用操作と区別がついていないことです。
ステップ4:override で潰す
追記するのはこの 4 行です。
- macro: user_expected_terminal_shell_in_container_conditions
condition: (k8s.ns.name = "fanclub" and k8s.pod.name startswith "fanclub-backend")
override:
condition: replace
足した後の rules-fanclub.yaml の最終形です(全量・これが本回の成果物です)。
- list: fanclub_shell_binaries
items: [sh, ash, bash, busybox]
- macro: fanclub_backend_pod
condition: (k8s.ns.name = "fanclub" and k8s.pod.name startswith "fanclub-backend")
- rule: Shell spawned in fanclub-backend pod
desc: fanclub-backend Pod のいずれかのコンテナ内でシェルが起動した
condition: >
spawned_process
and container
and fanclub_backend_pod
and proc.name in (fanclub_shell_binaries)
output: >
fanclub-backend Pod でシェル起動 (user=%user.name uid=%user.uid proc=%proc.name
exe=%proc.exepath cmd=%proc.cmdline tty=%proc.tty
container=%container.name
image=%container.image.repository:%container.image.tag
pod=%k8s.pod.name ns=%k8s.ns.name)
priority: WARNING
tags: [fanclub, shell, cks]
- macro: user_expected_terminal_shell_in_container_conditions
condition: (k8s.ns.name = "fanclub" and k8s.pod.name startswith "fanclub-backend")
override:
condition: replace
マクロの条件は、演習③で書いた fanclub_backend_pod とまったく同じ形です。 「自分たちが監視対象として定義した範囲では、対話端末付きのシェルは想定内である」という意味になります。
ここで fanclub_backend_pod マクロを参照しないのは、除外の条件と検知の条件を独立させておくためです。片方を広げたときに、もう片方まで一緒に動いてしまうのを防ぎます。
実行コマンド(k8s-ops・developer):
$ kubectl edit configmap -n falco falco-rules
反映は 31 秒でした(実測)。schema validation: ok が出ることを必ず確認します。
実行コマンド(k8s-ops・developer):
$ kubectl logs -n falco <k8s-wl-01 上の falco Pod 名> -c falco --tail=12 | grep -E 'Loading rules|schema validation'
実行結果(実測):
Sat Aug 01 16:28:34 2026: Loading rules from:
Sat Aug 01 16:28:34 2026: /etc/falco/falco_rules.yaml | schema validation: ok
Sat Aug 01 16:28:34 2026: /etc/falco/rules.d/rules-fanclub.yaml | schema validation: ok
override の記法を誤ると、ルール全体が読み込まれず、それまで動いていたカスタムルールごと止まります。 「1 行足しただけ」でも、読み込み結果を確認してから次へ進みます。
ステップ5:3 通りで確認する
チューニングの後に確認すべきことは「静かになったこと」ではありません。 必要な検知が残っていることです。3 通りで確かめます。
実行コマンド(k8s-ops・developer):
$ kubectl exec -it -n fanclub <backend Pod 名> -c backend -- sh -c 'echo after-1'
$ kubectl exec -it -n fanclub <backend Pod 名> -c backend -- sh -c 'echo after-2'
$ kubectl exec -it -n fanclub <backend Pod 名> -c backend -- sh -c 'echo after-3'
$ kubectl exec -it -n fanclub <frontend Pod 名> -c frontend -- sh -c 'echo frontend-it'
実行結果(通算の増分・実測):
| 数えるもの | 増分 | 意味 |
|---|---|---|
attached terminal(既定ルール)を backend で | 0 | 潰したかったノイズが消えました |
Pod でシェル起動(自分のカスタムルール) | 3 | 自分で作った検知は生きています |
attached terminal を frontend で | 1 | 絞った範囲の外では、既定ルールがそのまま働いています |
3 行目が重要です。 override はルールを無効にしたのではなく、条件を狭めただけです。同じ namespace の別の Pod では、既定ルールは今までどおり発火します。
もし 3 行目が 0 だったら、絞りすぎです。 「静かになった」ことを成功と読むと、この違いが見えません。
ステップ6:values.yaml へ書き戻す(この演習を「残す」ための必須手順)
ここまでの 2 回の変更(演習③の pod 版ルール・演習④の override)は、すべて ConfigMap を直接編集して入れたものです。 それは実験と緊急対応のための手段でした。残すなら values.yaml が正でなければなりません。
~/falco-lab/custom-values.yaml を最終形へ更新します(全量)。
customRules:
rules-fanclub.yaml: |-
- list: fanclub_shell_binaries
items: [sh, ash, bash, busybox]
- macro: fanclub_backend_pod
condition: (k8s.ns.name = "fanclub" and k8s.pod.name startswith "fanclub-backend")
- rule: Shell spawned in fanclub-backend pod
desc: fanclub-backend Pod のいずれかのコンテナ内でシェルが起動した
condition: >
spawned_process
and container
and fanclub_backend_pod
and proc.name in (fanclub_shell_binaries)
output: >
fanclub-backend Pod でシェル起動 (user=%user.name uid=%user.uid proc=%proc.name
exe=%proc.exepath cmd=%proc.cmdline tty=%proc.tty
container=%container.name
image=%container.image.repository:%container.image.tag
pod=%k8s.pod.name ns=%k8s.ns.name)
priority: WARNING
tags: [fanclub, shell, cks]
- macro: user_expected_terminal_shell_in_container_conditions
condition: (k8s.ns.name = "fanclub" and k8s.pod.name startswith "fanclub-backend")
override:
condition: replace
ここで素直に helm upgrade を打つと、失敗します。 失敗の仕方に本回で最も重要な論点があるので、次の節で扱います。
直接編集したものは、そのままでは Helm に戻せません
実行コマンド(k8s-ops・developer):
$ helm upgrade falco oci://ghcr.io/falcosecurity/charts/falco --version 9.1.0 --namespace falco -f ~/falco-lab/custom-values.yaml
実行結果(実測):
Error: UPGRADE FAILED: conflict occurred while applying object falco/falco-rules /v1, Kind=ConfigMap: Apply failed with 1 conflict: conflict with "kubectl-client-side-apply" using v1: .data.rules-fanclub.yaml
原因は、Kubernetes がフィールドごとに「誰が書いたか」を覚えているためです。 kubectl で直接編集した時点で、.data.rules-fanclub.yaml の所有者が kubectl-client-side-apply になりました。Helm は Server-Side Apply で書こうとしますが、そのフィールドは自分のものではないので拒否されます。
Server-Side Apply は、kubectl 側ではなく API サーバ側でマージを行う方式です。 どのフィールドを誰(field manager)が書いたかをオブジェクトに記録するところが、従来のクライアント側 apply と違います。その記録が metadata.managedFields です——次のコマンドで実際に見えます。
実行コマンド(k8s-ops・developer):
$ kubectl get cm falco-rules -n falco -o jsonpath='{range .metadata.managedFields[*]}manager={.manager} op={.operation}{"\n"}{end}'
実行結果(実測):
manager=helm op=Apply
manager=kubectl-client-side-apply op=Update
管理者が 2 人いる状態です。 これが衝突の正体です。
ここが本回でいちばん見逃しやすい箇所です。
helm upgrade が失敗しているのに、kubectl rollout status ds/falco -n falco は successfully rolled out を返します。 5 台すべてで schema validation: ok も出ます。
理由を取り違えないでください。 「ConfigMap で止まったから DaemonSet は無事」ではありません。実測では、この失敗した helm upgrade でも DaemonSet は更新され、5 台がローリング再起動しています。
$ kubectl get ds falco -n falco -o jsonpath='{.metadata.generation}'
3
$ kubectl rollout history ds/falco -n falco
REVISION CHANGE-CAUSE
1 <none>
2 <none>
3 <none>
generation が 3 に上がっています(1 = install / 2 = 演習③ の helm upgrade / 3 = この「失敗した」helm upgrade)。Pod の AGE も 5 台とも数十秒〜2 分台で、作り直されたことが分かります。Helm 4 は DaemonSet を先に適用し、そのあと ConfigMap の衝突だけを報告して failed にしています。
したがって rollout status は嘘をついていません——DaemonSet は本当にロールし終えています。問題は、それが「Helm が成功したか」の答えになっていないことです。見るべきは helm list か helm history です。
そして実害がもう 1 つあります。 本回の別の節で「helm upgrade は DaemonSet を作り直します。5 台が順番に落ちて上がる間、そのノードは監視されていません」と述べたあの空白が、失敗した upgrade でも発生しているということです。「失敗したから何も起きていない」と読むと、検知が止まっていた 2 分に気づけません。
実行コマンド(k8s-ops・developer):
$ helm history falco -n falco
実行結果(実測・抜粋):
REVISION STATUS DESCRIPTION
2 deployed Upgrade complete
3 failed Upgrade "falco" failed: conflict occurred while applying object falco/falco-rules
deployed はまだ REVISION 2 のままです。 つまり ConfigMap は直接編集した内容のままで、Helm の管理下には戻っていません。
解決は、ConfigMap をいったん削除してから helm upgrade を打つことです。
実行コマンド(k8s-ops・developer):
$ kubectl delete cm falco-rules -n falco
$ helm upgrade falco oci://ghcr.io/falcosecurity/charts/falco --version 9.1.0 --namespace falco -f ~/falco-lab/custom-values.yaml
$ helm list -n falco
実行結果(実測・抜粋):
Release "falco" has been upgraded. Happy Helming!
REVISION: 4
STATUS: deployed
helm list -n falco の側はこう見えます(実測)。
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
falco falco 4 2026-08-02 01:32:34.322339738 +0900 JST deployed falco-9.1.0 0.44.1
upgrade 自体は 1 秒で終わりました(実測)。ConfigMap の中身が values.yaml と同じなので、DaemonSet のチェックサムが変わらず、ローリング再起動も起きません。
所有者が戻ったことを確認します。
実行コマンド(k8s-ops・developer):
$ kubectl get cm falco-rules -n falco -o jsonpath='{range .metadata.managedFields[*]}manager={.manager}{"\n"}{end}'
実行結果(実測):
manager=helm
本番ガードレール⑪: 削除している数秒間、そのルールは存在しません。
kubectl delete cm から helm upgrade が終わるまでの間、Falco はカスタムルールを失った状態になりえます。 本書ラボでは 1 秒でしたが、手で打つと数秒から数十秒になります。
本番では、2 つのコマンドを 1 行にまとめて打ちます。 そしてこの作業自体を、検知が途切れても構わない時間帯に行います。
そもそも、この事態を避ける方法があります——直接編集を実験で終わらせ、恒久的な変更は最初から values.yaml で入れることです。 本回は「2 つの経路の違い」を体験するために直接編集を使いましたが、実務では実験が終わった時点で values.yaml へ戻すのが早いです。
ConfigMap を直接編集した状態のままにすると、次に誰かが helm upgrade falco を打った瞬間、本回の成果が黙って消えます——今回のように失敗して止まってくれるとは限りません。ルールが減っても Falco はエラーを出しません。アラートが出なくなるだけです。 そしてアラートが出ないのは、平常時と見分けがつきません。
本回はこれを 3 回見てきました。 ①1 台だけ Falco が上がっていなくても静か ②tty を付けなければ既定ルールに映らない ③image で絞ると sidecar が映らない。「values.yaml へ書き戻さない」は、4 つ目の同じ失敗です。
第16回以降はこのルールが入っている前提で進みます。 残す判断をしたなら、残る形にするまでが作業です——演習①の sysctl 永続化とまったく同じ話です。
ステップ7:差し込んだデバッグコンテナを片付ける
ephemeral container は削除できません。 前の節で kubectl debug を試した Pod には、失敗したまま残っているコンテナがあります。
実行コマンド(k8s-ops・developer):
$ kubectl get pod -n fanclub <backend Pod 名> -o jsonpath='{range .status.ephemeralContainerStatuses[*]}{.name}={.state}{"\n"}{end}'
実行結果(実測・抜粋):
debugger-fl55t={"waiting":{"message":"container has runAsNonRoot and image will run as root ...","reason":"CreateContainerConfigError"}}
この Pod を describe するたび、以降ずっとこのエラーが目に入ります。 片付ける唯一の方法は Pod を作り直すことです。
実行コマンド(k8s-ops・developer):
$ kubectl delete pod -n fanclub <ephemeral container が残っている backend Pod 名>
Deployment が新しい Pod を作り直し、ephemeral container の記録も一緒に消えます。 この Pod は Deployment 配下なので削除すれば作り直されます——Deployment / StatefulSet の配下でない Pod を同じ手で消すと、戻りません。 調査のために差し込んだものを、調査が終わったら片付ける——第18回のインシデント対応でも同じ作法が要ります。
演習④のゴール確認
| 確認 | 期待(実測) |
|---|---|
kubectl exec -it を 3 回 → 既定ルール | +3 件(100% 再現します) |
| override 適用(反映) | 31 秒・schema validation: ok |
override 後に exec -it を 3 回 → 既定ルール | +0 件 |
| 同時に自分のカスタムルール | +3 件(生存) |
frontend Pod で exec -it → 既定ルール | +1 件(範囲外では生きています) |
そのまま helm upgrade | 失敗(conflict with "kubectl-client-side-apply") |
ConfigMap を削除してから helm upgrade | 1 秒・REVISION 4 deployed・manager が helm 単独へ |
| ノードと Pod | 5/5 Ready・異常 0 |
https://fanclub.local/api/members | 200 + 会員 3 名 |
本番ガードレール⑫: 誤検知を消すときは、消した記録を残します。
override は「この挙動は正常だと判断した」という業務上の決定であり、技術的な設定変更ではありません。
残すべきものは 4 つです——①いつ ②誰が ③どのアラートを ④なぜ正常と判断したか。 本回はステップ 6 で values.yaml へ書き戻すところまでやりました。本番ではその values.yaml を Git 管理し、コミットメッセージに「なぜ正常と判断したか」を書きます(第14回で scan-gate.sh の変更を GitOps リポジトリへコミットしたのと同じ扱いです)。設定の変更履歴が、そのまま判断の記録になります。
override の条件は狭く書きます。 本例では k8s.ns.name と k8s.pod.name の 2 つで、1 つの Deployment が作る Pod だけに限定しました。namespace 全体や「コンテナならすべて」といった広い条件で潰しません。 広く潰すと、同じ namespace で起きた本物の攻撃も消えます。
そして「潰す」以外の選択肢を先に検討します。 誤検知を消す前に、①priority を下げる ②アラートの転送先を分ける(第17回)③そもそもその操作をやめられないか(kubectl exec -it に頼らない運用にできないか)を考えます。消すのは最後です。
試験ではこう問われる。
「指定のルールが指定のコンテナでは発火しないように調整せよ」という粒度で出ます。手が覚えているべきは、①既定ルールのチューニング用マクロを探す ②override: condition: replace で置き換える、の 2 つです。
falco.org/docs は参照できるので、override の記法(replace / append)は引けます。 ただし「どのマクロを置き換えるか」は既定ルールを読まないと分からないので、grep -E 'user_known|user_expected' /etc/falco/falco_rules.yaml のような探し方を手が覚えておきます。マクロ名の接頭辞は user_known_ と user_expected_ の 2 通りがあります。
既定ルールを直接編集して条件を足す解答も動きますが、避けてください。 ルールの更新で消えるためであり、試験でも「元のルールを壊さずに例外を足す」ほうが意図に沿います。
Helm の field manager 衝突は試験には出ません。 試験では Falco が Helm で入っているとは限らず、/etc/falco/ を直接編集する形が普通です。ここは本書ラボが「Helm で入れて、直接編集もした」からこそ起きた話です。
監視する側は何に守られているか —— Falco 自身が spc_t である
検知基盤そのものの権限を確認して、実装の話を締めます。
Falco は特権コンテナである
実行コマンド(k8s-ops・developer):
$ kubectl exec -n falco <falco Pod 名> -c falco -- cat /proc/self/attr/current
実行結果:
system_u:system_r:spc_t:s0
第6回で containerd の enable_selinux = true にして、コンテナが container_t:s0:cN,cM になることを確認しました。 それでも Falco は spc_t(super privileged container)です。
理由は privileged: true だからです。 特権コンテナは type enforcement の対象外になります。第6回でやったハードニングは、Falco には効いていません。
これは不具合ではありません。 Falco はカーネルの syscall ストリームを読む必要があり、SELinux で拘束されると仕事ができません。 監視の道具は、監視対象より強い権限を持たないと監視できません。
衝突は起きない(第10回・第11回との違い)
第10回と第11回では、第6回の SELinux が原因で新しいツールが動きませんでした。 本回は衝突しません。 ただし衝突しない理由が「特権だから」です——制約を回避しているだけであり、安全だから衝突しないのではありません。
falco namespace に PSA ラベルは付いていない
実行コマンド(k8s-ops・developer):
$ kubectl get ns falco --show-labels
実行結果(実測):
NAME STATUS AGE LABELS
falco Active 4h51m kubernetes.io/metadata.name=falco,name=falco
第8回で fanclub に enforce: restricted を付けましたが、falco には付けていません。 付けると privileged: true の Pod が admission で弾かれて Falco が動きません。
PSA を全 namespace に一律で適用できないのは、このように正当な理由で特権が要るワークロードがあるからです。 第8回で扱った「まず PSS、足りない制約だけ Gatekeeper / CEL」という順序の中で、「例外を作る namespace を決める」ことも設計の一部です。
例外を作ったら、その namespace に何を置けるかを RBAC で絞ります。 falco namespace に Pod を作れる人は、実質的にノードへの特権アクセスを持ちます——第4回の RBAC 監査の対象になります。
検知基盤の権限
| 項目 | 実測 | 意味 |
|---|---|---|
| SELinux ラベル | system_u:system_r:spc_t:s0 | type enforcement の外 |
privileged | true | ホストとほぼ同等の権限 |
falco namespace の PSA | ラベルなし | 付けると動きません |
| CPU / メモリ | 4〜14m / 106〜154Mi | 監視の代償としては小さい値 |
| syscall drop | 0 件 | 取りこぼしなし |
本番ガードレール⑬: 監視の道具が、監視対象より強い権限で動いていることを認識します。
Falco の Pod を作れる権限は、実質的にノードの root 相当です。
本番で確認する項目は 3 つあります——①falco namespace への書き込み権限を持つのは誰か ②Falco のイメージがどこから来たか(falcoctl は ghcr.io から artifact も取ります)③values.yaml の変更が誰の承認を経るか。
検知基盤を攻撃者に押さえられると、検知が止まるだけでなく、ノードへの足がかりを与えることになります。 第18回のインシデント対応では、「検知基盤自体が無事か」を最初に確認します。
暗記必須コマンドと、ラボ固有の事情の切り分け
試験中に参照できるものと、できないもの
CKS の試験中に参照できるのは 8 件だけで、うち 3 件はサブパス限定です。
| 参照先 | 可否 |
|---|---|
kubernetes.io/docs / kubernetes.io/blog | 可 |
falco.org/docs | 可(本回の主役) |
etcd.io/docs | 可 |
kubernetes-sigs.github.io/bom/cli-reference/ / kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/ / docs.cilium.io/en/stable | 可(サブパス限定・この 3 件) |
istio.io/latest/docs/ | 可 |
helm.sh/docs | 不可(CKAD / CKA では可・CKS では不可) |
sigstore.dev / trivy.dev / kubesec.io / docs.kubelinter.io / gvisor.dev | 不可(第12回〜第14回で確認済み) |
第12回(Kubesec / KubeLinter)・第13回(Trivy)・第14回(Cosign)は、いずれも参照不可の道具でした。 3 回続けて「引けないから覚える」を書いてきました。
本回は逆です。 falco.org/docs は 8 件のうちの 1 つで、ルールの記法もフィールド名も引けます。
それでも覚えておく価値があるものは残ります——①引く時間を減らすためのフィールド名 ②kubectl 側の打ち方(Falco のドキュメントには載っていません)③Helm のコマンド(helm.sh/docs は引けません)の 3 つです。
暗記必須コマンド
| 用途 | コマンド / 記法 | 補足 |
|---|---|---|
| アラートを見る | kubectl logs -n falco -l app.kubernetes.io/name=falco -c falco -f --prefix | 暗記(falco.org/docs には載っていません)。--prefix でノードを識別します |
| 検知を起こす(既定ルール) | kubectl exec -it -n <ns> <pod> -c <container> -- sh | 暗記。-it が無いと Terminal shell in container に当たりません |
| ルールの置き場所を調べる | grep -A5 rules_files /etc/falco/falco.yaml | falco.org/docs で引けますが、実機を見るほうが速い経路です |
| ルールの 3 要素 | - rule: / - macro: / - list: | 暗記。rule は desc / condition / output / priority / tags |
| priority 8 段階 | EMERGENCY / ALERT / CRITICAL / ERROR / WARNING / NOTICE / INFO / DEBUG | 参照可(順序は falco.org/docs が出典)ですが、覚えたほうが速いところです。既定 25 本で実際に使われているのは WARNING 11 / NOTICE 7 / CRITICAL 3 / INFO 1 の 4 種でした |
| 既定マクロ | spawned_process(プロセス起動)/ container(コンテナ内) | 暗記。カスタムルールの起点 |
| よく使うフィールド | proc.name / proc.exepath / proc.cmdline / proc.tty / user.name / user.uid / fd.name / container.id / container.name / container.image.repository / container.image.tag / k8s.pod.name / k8s.ns.name | 参照可ですが、引く時間が惜しいところです |
output の記法 | %フィールド名(例: %proc.cmdline) | 暗記 |
| 文字列の比較演算子 | = / != / contains / startswith / endswith / in (...) | 暗記 |
| 例外の書き方(override) | - macro: <既存名> + condition: + override: condition: replace | 暗記。replace は既定の条件を破棄し、append は既定に足します(本文で解説) |
| チューニング用マクロを探す | grep -E 'user_known|user_expected' /etc/falco/falco_rules.yaml | 暗記(どのマクロを置き換えるかを探す手順)。接頭辞は user_known_ と user_expected_ の 2 通りあります——片方だけで探すと取りこぼします |
| ルールの反映(DaemonSet・Pod を落とす) | helm upgrade <rel> oci://<ref> --version <ver> -n <ns> -f values.yaml | helm.sh/docs は CKS では参照不可 |
| ルールの反映(DaemonSet・Pod を落とさない) | ConfigMap を更新 → watch_config_files: true で自動リロード(1 分前後・最大 2 分弱) | 暗記 |
| ルールの反映(systemd 構成) | systemctl restart falco(または Falco プロセスへ SIGHUP) | 暗記。試験環境がこちらの可能性があります |
| 読み込み結果の確認 | ログの Loading rules from: と schema validation: ok | 暗記。ConfigMap を直接更新したときはリロードのログが 2 回出ます(helm upgrade 経由では 1 回) |
| 選ばれたドライバの確認 | cat /etc/falco/config.d/engine-kind-falcoctl.yaml | ラボ固有寄りの項目 |
| ephemeral container | kubectl debug <pod> -it --image=<img> --target=<container> | 暗記(kubernetes.io/docs で引けます)。--profile=restricted の存在も覚えます |
ラボ固有の事情(試験では 1 つも要りません)。
本回で何度も出てきた次の作業は、すべて「本書ラボの事情」です。
| 使ったもの | 本ラボで必要な理由 | 試験では |
|---|---|---|
oci://ghcr.io/falcosecurity/charts/falco | falcosecurity.github.io が whitelist に無い(Forbidden)から | 不要(そもそも Falco は導入済みで出題されます) |
fs.inotify.max_user_instances の引き上げ | k8s-wl-01 の Pod 密度が高く、上限 128 に対して 144 使っているから | 不要 |
/etc/sysctl.d/ への永続化 | 本回の Falco を第17回・第18回まで残すため | 不要 |
--profile=restricted --custom=<file>(kubectl debug) | fanclub namespace が PSA enforce: restricted で、busybox が root で動くから | 条件次第(--profile の存在は覚える価値があります。--custom までは試験で要りません) |
| ConfigMap の直接編集による反映 | chart で DaemonSet として入れているから | 構成次第(systemd 構成なら systemctl restart falco) |
override の対象が fanclub-backend Pod | 本ラボの模擬アプリが fanclub namespace にあるから | 不要(試験では別の題材になります) |
Helm の field manager 衝突(kubectl delete cm → helm upgrade) | chart で入れたものを kubectl でも直接編集したから | 不要(helm.sh/docs は CKS では参照できず、Helm の運用そのものが出題範囲外です。ただし「成功表示は成功を意味しない」という読み方は試験でも効きます) |
試験で打つのは、次の形だけです。
kubectl logs -n <ns> -l <label> -c falco
kubectl exec -it -n <ns> <pod> -- sh
そして書くのは、ルールの 3 要素です。
- list: <名前>
items: [...]
- macro: <名前>
condition: (...)
- rule: <名前>
desc: ...
condition: (spawned_process and container and ...)
output: "... (%proc.cmdline %container.name %k8s.pod.name)"
priority: WARNING
tags: [...]
ラボの事情を暗記対象と取り違えないでください。 本書が OCI 経路を使い、sysctl を触っているのは、閉じたネットワークと限られたノード数で同じことを再現するためです。試験環境では素の形が動きます。 ただし「なぜこの作業が要ったのか」を理解しておくことには意味があります——実務では、出口制御のあるネットワークや、Pod 密度の高いノードに出くわします。
alias k=kubectl について
CKS の SSH ホストには alias k=kubectl と bash 補完が設定済みです。本文では読みやすさのために kubectl と全綴りで書いていますが、試験ではエイリアスを使ってください。 加えて export do="--dry-run=client -o yaml" を開始直後に設定しておくと、マニフェストの雛形生成が速くなります。
本回はマニフェストを書く場面が少ない(ルールは YAML ですが Kubernetes のオブジェクトではありません)ので、do の出番は限られます。それでも kubectl logs / kubectl exec は何度も打つので、エイリアスの効果は大きいところです。
「入れない判断・外す判断・残す判断」の系譜
本巻はここまで、入れないと決めたものと入れて外したものを積み上げてきました。
残す判断をするのは本回が初めてです。 理由は第17回と第18回が土台にするからであり、残すために sysctl の永続化まで作業しました。
「入れる / 入れない」だけでなく「入れたものを維持できるか」まで含めて判断します——これが本巻が繰り返してきた設計の話です。
第16回への 1 行
本回で コンテナの中で起きたことを syscall のレベルで捉えました。 アラートには「何が起きたか」は出ますが、「誰がやったか」は出ません。 kubectl exec を打った人間は、コンテナの中からは見えないからです。第16回では、Kubernetes API 側の記録を扱います。
まとめ
- Falco は syscall を見ます。 プロセスの起動・ファイルのオープン・ネットワーク接続を捉えます。Kubernetes API に対する操作は見ていません——それは第16回の監査ログの担当です
- ドライバは
autoでmodern_ebpfが選ばれました。/sys/kernel/btf/vmlinuxがあるので CO-RE が成立し、download.falco.orgへのアクセスは 1 度も発生しません(No artifacts needed for the selected driver.)。当初想定していた whitelist の追加は不要でした helm repo add falcosecurity ...はForbiddenで通りません。 OCI(oci://ghcr.io/falcosecurity/charts/falco --version 9.1.0)なら通ります。 第13回・第14回に続き、whitelist は 44 行のままです- 依存 3 chart(falcosidekick 0.12.1 / k8s-metacollector 0.1.10 / falco-talon 0.3.0)は OCI アーティファクトに同梱されています。
Chart.lockが到達できない URL を指していても install は成立します。設定に書いてある URL が必ず使われるとは限りません helm installが成功しても、5 台中 4 台しか起動しないことがあります。Error: could not initialize inotify handler。上限fs.inotify.max_user_instances = 128に対し、k8s-wl-01 は 144 使っていました(cp-01 は 67 / wl-02 は 96)。containerd-shimが 1 プロセスあたり 2〜4 インスタンス使い、効くのはプロセスの個数(= ノードの Pod 密度)なので、Pod が多いノードから先に枯渇しますsysctl -w fs.inotify.max_user_instances=512で直り、Pod 削除から 45〜61 秒でRunningになりました。sysctl -wはノード再起動で戻るので、/etc/sysctl.d/へ永続化します——本回の Falco を残す判断には、残せる状態にする作業が伴いますmax_user_watchesは 60,255 で余裕があります。 枯渇しているのはinstancesのほうです。名前が似ているので取り違えないでください- falcoctl が ghcr.io からルールとプラグインを取り、cosign で署名を検証しています(
Verifying signature for artifact→Signature successfully verified!)。第14回で手で打った検証が、道具の内部で自動的に走っています - falcoctl の index(
falcosecurity.github.io/falcoctl/index.yaml)には到達できませんが、artifact の取得先が ghcr.io なので install は成功します k8s_pod_name/k8s_ns_nameは container プラグイン 0.7.1 が付けています。 k8s-metacollector は不要です- 既定ルールは 25 本しかありません。 攻撃の流れ(入る → 調べる → 外へ出す → 消す → 上がる)に沿って並んでいます。「Falco を入れれば検知される」ではなく「25 本の網が張られる。足りない分は自分で書く」が正しい理解です
rule_matchingはfirst(最初にマッチしたルールで打ち切る)、priorityの出力しきい値はdebug(全優先度を出す)、rules_filesはfalco_rules.yaml→falco_rules.local.yaml→rules.dの順です- 素の
kubectl execは既定ルールに引っかかりません。Terminal shell in containerにはproc.tty != 0があり、-itを付けないと発火しません(実測:terminal=34816)。「記事どおりに打ったのに何も出ない」の正体はここにあります - 絞り込みは、常に「見なくなるもの」を作ります。 既定ルールが tty で絞っているのは誤検知を減らすためですが、tty を使わない操作は 1 件も映りません
- chart の
customRulesは/etc/falco/rules.d/<キー名>に置かれます。falco_rules.local.yamlではありません。実体は ConfigMap の symlink です - 反映の経路が 2 つあります。
helm upgradeは DaemonSet をローリング再起動します(5 台で約 2 分・その間そのノードは監視されません)。ConfigMap の直接更新は Pod を落とさず自動リロードされました(通算 6 回の計測で 20 / 31 / 41 / 51 / 77 / 87 秒——すべて違いました。「1 分前後・最大 2 分弱」と見込みます) - ConfigMap を直接更新したときは、リロードのログが 2 回出ます(
helm upgrade経由では 1 回です)。ConfigMap の symlink 差し替えで inotify イベントが複数発火するためで、壊れているのではありません。 見るべきはschema validation: okの側です - カスタムルールは tty が無くても検知します(実測:
tty=0/exe=/bin/busybox)。proc.nameはshでも実体は busybox なので、outputにproc.exepathを入れておきます container.image.repositoryで絞ったルールは、同じ Pod の sidecar を見逃します。 backend Pod にはlog-shipper(busybox:1.37・native sidecar)が同居しており、そちらでshを起動しても検知されませんでした。k8s.pod.nameで絞る版に直すと両方が検知されます- sidecar は攻撃者にとって都合がよい存在です。
busyboxにはshもwgetもncも入っています。第12回で本体を最小化しても、同じ Pod に busybox が居れば効果は薄くなります kubectl debugは PSA restricted の namespace で 3 段階に詰まります。 ①既定(legacy)は admission でForbidden(3 点の PSA 違反)②--profile=restrictedは kubelet でCreateContainerConfigError(runAsNonRootはあるがrunAsUserが無く busybox は root で動く)③--customでrunAsUser: 10001を渡して起動(uid=10001 gid=0(root))--targetで PID 名前空間を共有しても/proc/1/root/はPermission deniedです。 見えるのはプロセスの一覧までで、ファイルを辿るには権限が要ります- 失敗した ephemeral container は削除できません。 Pod を作り直すまで残ります。プロファイルは打つ前に決めておきます
- ephemeral container は image で絞ったルールから漏れます——sidecar と同じ理由です
- 誤検知は「待つ」のではなく「起こしてから測る」。 通算 0 件 →
kubectl exec -itを 3 回 → +3 件(100% 再現します)→ override 適用(反映 31 秒)→ もう一度 3 回打って +0 件 → 同時に自分のカスタムルールは +3 件(生存) → frontend Pod では +1 件(絞った範囲の外では既定ルールが生きています) - 「変更前 0 件 → 変更後 0 件」では何も言えません。 効果を測るときは、変更前に必ず 1 以上を出しておきます。第13回・第14回に続く「測りたいものを測る」の 3 例目です
- 既定ルールにはチューニング用の空マクロ(既定
(never_true))が用意されています。and not user_known_◯◯_activities(あるいはuser_expected_◯◯_conditions)をoverride: condition: replaceで置き換えます。接頭辞は 2 通りあるので、探すときはgrep -E 'user_known|user_expected'です。既定ルールを直接書き換えません - チューニングの後は「静かになったこと」ではなく「必要な検知が残っていること」を確認します
- 本ラボに実在した誤検知は 2 つとも演習に使えませんでした。 longhorn-manager 側は wl-01 のみ 20 分で 6 件と間欠的で、
Contact K8S API Server From Container側はfd.sip.name(接続先の逆引き名)に依存するのでノードとタイミングで出たり出なかったりしました。演習には、読者が確実に再現できる操作(kubectl exec -it)を題材に選びました - Falco 自身は
spc_tです(privileged: true)。第6回の SELinux 拘束の外にいます。 第10回の gVisor・第11回の SPIRE のような衝突は起きませんが、衝突しない理由は「特権だから」です falconamespace に PSA ラベルは付いていません。 付けるとprivilegedが通りません。例外を作る namespace は、RBAC で誰が書けるかを絞ります- 検知基盤の負荷は CPU 4〜14m / メモリ 106〜154Mi・syscall drop 0 件です。 drop が出ると、その分アラートが出ません——「静かだから安全」の 3 つ目の原因になります
- ConfigMap を直接編集したものは、そのままでは
helm upgradeで戻せません。 フィールドの所有者がkubectl-client-side-applyになり、Helm の Server-Side Apply がconflictで拒否されます。しかもkubectl rollout statusはsuccessfully rolled outを返すので失敗が表に出ません(DaemonSet には触れていないため当然です)。成否はhelm listかhelm historyで見ます。 解決は ConfigMap をいったん削除してからhelm upgrade(実測 1 秒・REVISION 4deployed・manager がhelm単独へ戻る) - 本回で入れた Falco は残します。 第10回の gVisor・第14回の ImagePolicyWebhook は撤去しましたが、本回は第17回・第18回が土台にします
- 本回の軸: 検知の仕組みは、検知しなかったときに何も言いません。 「アラートが出ていない」には何も起きていないと見ていないの 2 つの意味があります。本回は 4 回、その区別を実機で作りました——①1 台だけ起動していない ②tty が無いと既定ルールに映らない ③image で絞ると sidecar が映らない ④
helm upgradeが失敗してもrollout statusは成功と言う - 本回も whitelist を 1 行も足していません(44 行のまま・3 回連続です)
第19回で復習する項目。
falco.org/docs は試験中に参照できるので、Falco は「全体が暗記対象」ではありません。 第19回に送るのは ①rule / macro / list の 3 要素のキー名 ②condition の spawned_process and container 型 ③output の %フィールド名 記法 ④override: condition: replace の 4 点です。OCI 経路・inotify・--custom はラボ固有なので送りません。
理解度チェック(○×形式・全 9 問)
次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。
- Falco は syscall を監視するため、
kubectl get secretのような Kubernetes API に対する操作も検知できる modern_ebpfドライバは、カーネルに合わせた probe を外部からダウンロードする必要がないhelm installが成功していれば、DaemonSet の Pod は全ノードで起動していると考えてよい- Falco の既定ルール
Terminal shell in containerは、kubectl execに-itを付けなくても発火する - Helm chart の
customRulesに書いたルールは/etc/falco/falco_rules.local.yamlに配置される watch_config_filesが有効な構成では、ルールの ConfigMap を更新すれば Pod を再起動しなくても反映されるcontainer.image.repositoryで対象を絞ったルールは、同じ Pod 内で別のイメージが動いているコンテナも検知する- 既定ルールの誤検知を消すときは、
falco_rules.yamlの該当ルールのconditionを直接書き換えるのが正しい privileged: trueで動く Falco のコンテナは、containerd のenable_selinuxを有効にしていてもcontainer_tではなくspc_tになる
解答と解説
1=×/2=○/3=×/4=×/5=×/6=○/7=×/8=×/9=○
- 問1: Falco が見ているのはカーネルの syscall です。
kubectl get secretは API サーバへの HTTP リクエストであり、コンテナ内の syscall にはなりません。API 操作の記録は第16回の監査ログの担当です。2 つは守備範囲が違い、片方だけでは「誰が何をしたか」が揃いません——本回のアラートにuser=appuserは出ますが、kubectl execを打った人間の名前は出ません(parent=containerd-shimまでしか分かりません) - 問2:
modern_ebpfは CO-RE(Compile Once – Run Everywhere)で、BTF 情報を使って実行時にカーネルへ合わせます。 本ラボでは/sys/kernel/btf/vmlinux(5,652,577 バイト)が存在するため成立し、download.falco.orgへのアクセスは 1 度も発生しませんでした(No artifacts needed for the selected driver.)。kmodやebpfを選ぶと外部からの取得が必要になるので、出口制御のあるネットワークでは whitelist に追加が要ります。ドライバの選択が、そのままネットワーク要件を決めます - 問3: 考えてはいけません。 実測では
helm installは 2 秒で成功したのに、5 台中 1 台(k8s-wl-01)がCrashLoopBackOffでした。原因はfs.inotify.max_user_instancesの枯渇(上限 128 に対し 144 使用)で、containerd-shimが 1 プロセスあたり 2〜4 インスタンス使い、効くのはプロセスの個数(= ノードの Pod 密度)なので、Pod が多いノードから先に枯渇します。 Helm のSTATUS: deployedは「chart を適用した」という意味でしかありません。 DaemonSet を入れたらkubectl get pods -o wideで全ノード分を数えます - 問4: 発火しません。 既定ルールの
conditionにproc.tty != 0があるためで、-itを付けないkubectl execは端末を割り当てないので当たりません(実測: 素のexecは 0 件 /-it付きはterminal=34816で発火)。ルール名の「Terminal shell」がそのまま条件を表しています。 これは実装の不備ではなく誤検知を減らすための意図的な絞り込みですが、絞り込みは常に「見なくなるもの」を作ります——tty を使わない操作は 1 件も映りません - 問5:
/etc/falco/rules.d/<キー名>に置かれます。customRules: {rules-fanclub.yaml: ...}と書けば/etc/falco/rules.d/rules-fanclub.yaml(実体は ConfigMap の symlink)になります。falco_rules.local.yamlはrules_filesには列挙されていますが、chart はこのファイルを作りません。 ホストに直接 Falco を入れた systemd 構成ではfalco_rules.local.yamlが定石なので、構成によって置き場所が変わります——迷ったら/etc/falco/falco.yamlのrules_filesを実機で見ます - 問6: 反映されます。
watch_config_files: trueが既定で、Falco が inotify でファイルの変更を監視しています(Falco が inotify を使う理由がこれであり、問3 の枯渇問題の原因でもあります)。実測の反映時間は 通算 6 回の計測で 20 / 31 / 41 / 51 / 77 / 87 秒と一定しない(kubelet の ConfigMap 同期周期次第)ので、「1 分前後・最大 2 分弱」を見込みます。 ConfigMap を直接更新したときはリロードのログが 2 回出ますが、symlink の差し替えで inotify イベントが複数発火するためで壊れてはいません(helm upgrade経由では 1 回です)。見るべきはschema validation: okの側です。なおhelm upgradeで反映すると DaemonSet がローリング再起動します(5 台で約 2 分・その間そのノードは監視されません) - 問7: 検知しません。 実測では、backend Pod の
log-shipper(busybox:1.37の native sidecar)でシェルを起動しても 1 件も出ませんでした。イメージがfanclub-backendではないためです。k8s.pod.nameで絞る版に直すと両方が検知されます。 ephemeral container(kubectl debugで差し込むコンテナ)も同じ理由で漏れます。 絞り方は「監視したい単位」を決めてから選びます——イメージ単位か、Pod 単位か、namespace 単位かで、見えなくなるものが変わります - 問8: 正しくありません。
falco_rules.yamlは falcoctl がルールを更新したときに上書きされるので、書き換えは消えます。既定ルールにはand not user_known_◯◯_activitiesやand not user_expected_◯◯_conditionsというチューニング用の空マクロ(既定(never_true))があらかじめ用意されているので、override: condition: replaceで同名マクロを再定義します。 本回の実測ではuser_expected_terminal_shell_in_container_conditionsをk8s.ns.name = "fanclub" and k8s.pod.name startswith "fanclub-backend"で置き換え、既定ルールは +0 件・自分のカスタムルールは +3 件・frontend Pod では +1 件でした。接頭辞はuser_known_とuser_expected_の 2 通りあるので、探すときはgrep -E 'user_known|user_expected'です。override の条件は狭く書きます(本例では namespace + Pod 名の 2 つで 1 Deployment に限定しました)。広く潰すと本物の攻撃も消えます - 問9:
spc_tになります。privileged: trueのコンテナは SELinux の type enforcement の対象外です。第6回でenable_selinux = trueにして通常のコンテナはcontainer_t:s0:cN,cMになりましたが、Falco はsystem_u:system_r:spc_t:s0のままでした。 第10回の gVisor・第11回の SPIRE は SELinux と衝突しましたが、Falco は衝突しません——衝突しない理由が「特権だから」である点は押さえておきます。監視の道具は監視対象より強い権限で動くので、falconamespace に Pod を作れる権限は実質ノードの root 相当です(第4回の RBAC 監査の対象になります)
次回予告
第16回「監査ログ設計・分析 + 攻撃フェーズ特定」では、Kubernetes API 側の記録を扱います。
本回のアラートには user=appuser(コンテナ内のユーザー)と parent=containerd-shim(外から差し込まれたこと)までは出ましたが、kubectl exec を打った人間の名前は 1 度も出ませんでした。 コンテナの中から、外の人間は見えないからです。
第16回は、その見えなかった側を埋めます。 Audit Policy を設計して kube-apiserver に適用し、誰が・いつ・何に対して・どの操作を打ったかを記録します。そして第2回で保留した kube-bench の FAIL(1.2.16〜1.2.19)が、3 点セットを入れれば PASS へ転じることも確認します。
Falco(syscall)と監査ログ(API 操作)の 2 つが揃うと、「何が起きたか」と「誰がやったか」が結びつきます。 ただし第16回では、その監査ログを回の終わりに撤去します——平文の危険と容量という代償を実測したうえでの判断です。第18回のインシデント対応は、その代償が実害に変わるところから始まります。
