おい、ちょっと手を止めてこっちを向いてくれ。
先日、プロダクション環境で動いているKubernetesクラスターのログを何気なく眺めていたら、背筋が凍るようなものを見つけたんだ。あるWebアプリケーションコンテナの中で、誰も実行した覚えのない sh が立ち上がり、外部のC2サーバー(コマンド&コントロールサーバー)へ向けたリバースシェルが張られかけていた。
幸いなことに、俺たちが導入していたランタイムセキュリティツールがその瞬間に検知し、即座にプロセスを強制終了(Kill)してアラートを飛ばしてくれたから大事に至らなかった。だが、もしあのとき検知が遅れていたら、今頃会社は大炎上し、俺たちは深夜の記者会見の台本を書かされていたことだろう。
「うちはイメージスキャンをやっているから大丈夫です」
「コンテナはイミュータブル(不変)だから侵入されても再起動すれば消えます」
……よく聞くセリフだが、現場を知るエンジニアならそれがどれほど甘い幻想か分かっているはずだ。ゼロデイ脆弱性や、依存ライブラリ(npmやPyPI)へのサプライチェーン攻撃、あるいはアプリケーション層のRCE(リモートコード実行)を許してしまえば、攻撃者は容易にコンテナの内部へ足場を築く。
今回は、コンテナが侵入された「その瞬間」を捉え、システムを守り抜くためのFalcoを用いたランタイムセキュリティの極意を叩き込む。教科書に書いてあるようなきれいごとは抜きで、現場で本当に使える実践的な設定と防御の仕組みを解説していこう。
—
1. なぜ「静的スキャン」だけではコンテナを守れないのか?
多くのチームは、CI/CDパイプラインにTrivyやClairといった脆弱性スキャナーを組み込んでいる。それはもちろん必須のファーストステップだ。しかし、これらはあくまで「既知の脆弱性を持ったパッケージが含まれていないか」を静的にチェックしているに過ぎない。
実際のインシデントでは、次のようなシナリオが日常茶飯事なのだ。
1. アプリケーションの脆弱性(例:不完全なファイルアップロードやデシリアライゼーション)を突かれ、Webサーバーのプロセス経由で任意のコードが実行される。
2. 攻撃者はコンテナ内でシェル(/bin/sh や /bin/bash)を起動し、特権昇格や内部ネットワークの探索を開始する。
3. 機密情報(環境変数にハードコードされたAWSクレデンシャルなど)を盗み出し、外へ持ち出す。
このプロセスにおいて、コンテナイメージ自体がクリーンであったとしても関係ない。動いている最中のランタイム(Runtime)で何が起きているかを監視できなければ、侵入されても指をくわえて見ていることしかできないのだ。ここで登場するのが、クラウドネイティブの守護神 Falco である。
—
2. Falcoの仕組み:カーネル空間から不正な挙動を検知する
Falcoは、シスコの傘下であるScyllaやSysdigが開発したオープンソースのランタイムセキュリティツールだ。こいつの何が強力かというと、アプリケーションのログを見るのではなく、Linuxカーネルのシステムコール(execve, open, connect など)を直接監視している点にある。
つまり、攻撃者がどれだけログを改ざんしようが、プロセス名を偽装しようが、カーネルレベルで発行されるシステムコールをごまかすことはできない。
Falcoが防ぐべき代表的な異常イベント
- コンテナ内での予期せぬシェル(
sh,bash,zsh)の起動 - 設定ファイルやバイナリ(
/etc/passwdや/bin/以下)の改ざん - コンテナ内から外部の未知のIPアドレスへの直接通信
- マウントされたホストの機密ディレクトリへのアクセス
—
3. 【実践】即座に導入・検知するためのFalcoカスタムルール設定
デフォルトのFalcoルールでも十分強力だが、現場のアプリケーション特性に合わせたカスタムルールを書くことで、誤検知を減らし、真の脅威だけに反応させることができる。
ここでは、「本番環境のWebコンテナ内でシェルが起動したこと」を検知し、さらにSlackへ即座にアラートを飛ばすための実用的な設定を行っていこう。
步驟1: カスタムルールの定義 (/etc/falco/rules.local.yaml)
まずは、コンテナ内での不審なプロセス実行を検知するカスタムルールを追加する。
# /etc/falco/rules.local.yaml
- macro: container_activity
condition: container.id != host and not container.id = host
- rule: Unexpected Shell Execution in Container
desc: Detect interactive shell execution inside a production container.
condition: >
spawned_process
and container_activity
and proc.name in (sh, bash, zsh, dash, ash)
and not k8s.pod.label.allow_exec = "true"
output: >
CRITICAL: Unauthorized shell spawned in container!
(user=%user.name command=%proc.cmdline container_id=%container.id container_name=%container.name image=%container.image.repository)
priority: WARNING
tags: [container, execution, security_incident]
【セキュリティチーフの現場解説】
このルールでは、container_activity でコンテナ内であることを絞り込み、proc.name で一般的なシェルを指定している。ポイントは not k8s.pod.label.allow_exec = "true" という例外処理だ。デバッグ目的でどうしてもKubernetesの kubectl exec を使いたいPodには、このラベルを付与することで、無駄なアラートの嵐を防ぐことができる。現場の運用のしやすさを考慮した実践的なテクニックだ。
—
4. アラートのリアルタイム通知と自動防御(Incident Response)
検知するだけではプロの仕事とは言えない。セキュリティインシデントが発生した際、人間の手による対応を待っていたのでは被害が拡大する。ここでは、検知と同時にSlackへ通知し、さらに悪質な場合はコンテナを強制終了させる仕組みを構築しよう。
步驟2: FalcoサイドカーとWebhookの設定 (/etc/falco/falco.yaml)
Falcoの出力先をカスタムWebhookに向け、Slackや独自のインシデントレスポンス基盤(AWS LambdaやPythonスクリプト)に飛ばす。
# /etc/falco/falco.yaml の抜粋設定
json_output: true
json_include_output_property: true
# ログファイルへの出力
file_output:
enabled: true
keep_alive: false
filename: /var/log/falco_events.json
# 標準出力へのJSON出力(Kubernetes環境ではこれがFluentdやDatadogに回収される)
stdout_output:
enabled: true
# Webhookによる外部連携(例: SOCやSlack通知用エンドポイント)
http_output:
enabled: true
url: "https://your-internal-security-webhook.example.com/alerts"
user_agent: "falcosecurity/falco"
—
5. アプリケーション層からの防御の補完:セキュアなコード設計
ランタイム監視は最後の砦だが、そこへ到達する回数自体を減らすことがエンジニアの腕の見せ所だ。例えば、PHPやPythonでWebアプリケーションを構築する際、OSコマンドインジェクションの脆弱性を完全に排除するためのコードパターンを確認しておこう。
以下は、安全な外部プロセスの実行方法を示したPythonのサンプルコードだ。絶対に shell=True を使ってはならない。
import subprocess
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/ping', methods=['POST'])
def secure_ping():
data = request.get_json()
target_ip = data.get('ip', '')
# 【重要】IPアドレスの形式チェック(簡易的なバリデーション)
# シェルインジェクションの余地を完全に断つため、正規表現等で厳格に検証する
import re
if not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', target_ip):
return jsonify({"error": "Invalid IP address format"}), 400
try:
# 【重要】shell=True は絶対に使用しない!
# リスト形式で引数を渡すことで、OSシェルを介さずに直接バイナリを実行する
result = subprocess.run(
['ping', '-c', '1', target_ip],
capture_output=True,
text=True,
timeout=5
)
return jsonify({
"output": result.stdout,
"status": "success" if result.returncode == 0 else "failed"
}), 200
except subprocess.TimeoutExpired:
return jsonify({"error": "Command execution timed out"}), 500
except Exception as e:
# 詳細なエラーメッセージを外部に出さない(情報漏洩の防止)
return jsonify({"error": "Internal server error"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
このコードでは、shell=True を排除し、入力を厳格にバリデーションしている。仮に攻撃者がリクエストに ; rm -rf / のような悪意ある文字列を混ぜ込んでも、ping コマンドはその文字列を「存在しないIPアドレス」として処理するだけで、シェルが起動する隙を一切与えない。
—
6. まとめ:セキュリティは「多層防御」で完結する
今回紹介したFalcoによるランタイム監視は、あくまでセキュリティモデルにおける「最後の防壁」だ。
1. 入口の防御: WAFや適切なWebアプリのバリデーション(OSコマンドインジェクションの排除)
2. イメージの防御: Trivy等によるCI/CDでの脆弱性スキャン
3. 実行時の防御: Falcoによるカーネルレベルでの異常検知と自動遮断
この3つが歯車のように噛み合ってはじめて、本当の意味で堅牢なインフラストラクチャが完成する。「うちは大丈夫」という慢心を捨て、今日からでもステージング環境にFalcoを導入し、自社のコンテナの中をのぞき見してみてほしい。きっと、予想もしなかった挙動や改善点が見つかるはずだ。
さて、コードレビューに戻るとしよう。セキュアなシステム構築を楽しんでくれ!
コメント