コンテナランタイム(containerd / CRI-O)の要塞化:Dockerを卒業したエンジニアが陥る「ランタイムの盲点」
おい、最近の Kubernetes クラスター構築やコンテナ基盤の刷新、お疲れ様。
「docker.sock のマウントをやめて rootless にしたから完璧です!」――後輩の君がドヤ顔でそう報告してくれたのは嬉しいが、少し待ってほしい。それ、本当に足元まで固まっているか?
近年のモダンなインフラストラクチャにおいて、Docker daemon は表舞台から姿を消し、containerd や CRI-O といったコンテナランタイムが直接カーネルと対話するようになった。これは抽象化レイヤーが減り、パフォーマンスやセキュリティ面で大きなメリットを生んだ。しかし同時に、「ランタイム層の設定ミスや脆弱性が、そのままホストカーネルの直撃につながるリスク」 が跳ね上がっているんだ。
今回は、数々のインシデント現場でコンテナエスケープの痕跡を目の当たりにしてきた私から、containerd および CRI-O の実戦的なハーデニング(要塞化)手法を、泥臭い設定ファイルのチューニングと共にお伝えしよう。教科書には載っていない、攻撃者の視点を取り入れた実践知だ。
—
1. 攻撃者が狙うコンテナランタイムの盲点
多くの開発者は「イメージの脆弱性スキャン(Trivy等)」や「KubernetesのPod Security Standards(PSS)」に気を取られる。しかし、真のクラッカーは、ランタイムそのものの設定不備や、ホストとコンテナの境界線を曖昧にするデフォルト挙動を突いてくる。
デフォルト設定が孕む恐怖
containerd の設定ファイルである /etc/containerd/config.toml や、CRI-O の /etc/crio/crio.conf は、インストール直後のデフォルト状態では「利便性(動くこと)」が最優先されている。
例えば、不要なプラグインの有効化、デバッグポートの開放、そして名前空間(Namespaces)やcgroupの分離が不十分なランタイム設定は、万が一アプリケーション層でリモートコード実行(RCE)の脆弱性が突かれた際、一瞬でホストOSの root 権限へと直結する。
特に恐ろしいのが、コンテナランタイムがホストのプロセス空間やネットワークスタックを誤って共有してしまう設定ミスだ。これにより、コンテナエスケープ(Container Escape)が容易になり、同一基盤で稼働する他のテナントや、最悪の場合は物理・仮想ホスト全体が蹂躙される。
—
2. containerd の要塞化実装(config.toml の最適化)
それでは、具体的な設定に踏み込もう。まずは containerd だ。
以下の設定は、不要な機能を削ぎ落とし、セキュリティ境界を極限まで高めた config.toml の実戦投入サンプルだ。
# /etc/containerd/config.toml
# セキュリティチーフが推奨するプロダクション環境向けハードニング設定
version = 2
[plugins]
# デバッグ用エンドポイントや不要なメトリクス収集プラグインの露出を防ぐ
[plugins."io.containerd.grpc.v1.cri"]
# 廃止予定の機能やデバッグ用シムの実行を禁止
enable_unstable_fp_extensions = false
# Podのサンドボックス(pauseコンテナ)イメージを固定し、改ざんされたイメージの利用を防ぐ
sandbox_image = "k8s.gcr.io/pause:3.8"
[plugins."io.containerd.grpc.v1.cri".containerd]
# ランタイムのデフォルトをruncに指定
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
# 【重要】コンテナプロセスがホストのユーザー名前空間を不正に利用することを防ぐ
SystemdCgroup = true
# シェルインジェクション等によるバイナリ改ざんを防ぐため、runcのバイナリパスを明示
BinaryName = "/usr/local/bin/runc"
# 利用しないサービス, プラグインの無効化(アタックサーフェスの縮小)
[plugins."io.containerd.gc.v1.scheduler"]
deletion_threshold = 0
# デバッグログレベルの調整(機微な情報やトークンがログに流出するのを防ぐ)
[debug]
level = "info"
format = "json"
# グローバルなセキュリティ制限
[stream_server]
# デバッグ用の非暗号化ストリーム通信ポートを制限
address = "127.0.0.1"
port = "0"
ssl_cert = ""
ssl_key = ""
この設定を適用した後は、必ず systemctl restart containerd でサービスを再起動し、意図しないポートが外に向かって開いていないかを ss -tulpn で確認する習慣をつけよう。
—
3. CRI-O の要塞化実装(crio.conf の最適化)
次は RedHat系や OpenShift などで主流となる CRI-O だ。
CRI-O は Kubernetes に特化している分、セキュリティポリシーの厳格化が設定ファイル一発で可能だ。 /etc/crio/crio.conf またはドロップインディレクトリ (/etc/crio/crio.conf.d/) に以下の設定を配置してほしい。
# /etc/crio/crio.conf.d/01-security-hardening.conf
# CRI-O ランタイムのセキュリティ強化設定
[crio.runtime]
# デフォルトで許可されるケーパビリティ(Capabilities)を最小限に絞る
# 攻撃者が特権昇格によく使う CAP_SYS_ADMIN や CAP_NET_RAW などを排除する
default_capabilities = [
"CHOWN",
"DAC_OVERRIDE",
"FSETID",
"FOWNER",
"SETGID",
"SETUID",
"SETPCAP",
"NET_BIND_SERVICE",
"KILL",
"AUDIT_WRITE"
]
# 【超重要】特権コンテナ(Privileged Container)の実行を原則禁止する
# 例外的に必要な場合でも、KubernetesのPodSecurityPolicyやValidatingWebhookで厳密に制御すべき
pids_limit = 1024
# ホストのネットワーク名前空間の共有をデフォルトで制限
manage_network_ns = true
[crio.image]
# イメージプル時の署名検証を強制(サプライチェーン攻撃対策)
# 不正なレジストリからの改ざんされたイメージの実行をブロック
signature_policy = "/etc/containers/policy.json"
さらに、ランタイムレベルでの脆弱性(CVE-2024-xxxx や過去の runc の脆弱性など)に備えるため、runc や crun といった低レベルランタイム自体のバージョンも、常に最新のパッチが当たっている状態を apt や dnf の自動アップデートとは別に、監視スクリプト等で担保しておく必要がある。
—
4. 現場で使える!ランタイム設定の自動監査スクリプト(Python)
「設定ファイルを書いたから終わり」では、セキュリティチームとしては落第点だ。本番環境にデプロイされたランタイムが、本当に意図したセキュアな状態になっているかをプログラムで継続的に検証(監査)する仕組みが必要になる。
以下の Python スクリプトは、ローカルホスト上の containerd の設定やプロセス状況をチェックし、セキュリティ上の不備があればアラートを上げる実用的な監査ツールだ。
#!/usr/bin/env python3
"""
containerd_audit.py
コンテナランタイム(containerd)のセキュリティ設定を自動監査するスクリプト
作成者: シニアセキュリティチーフ
"""
import os
import subprocess
import sys
import toml
CONTAINERD_CONFIG_PATH = "/etc/containerd/config.toml"
def check_root_privileges():
"""スクリプトがroot権限で実行されているか確認"""
if os.geteuid() != 0:
print("[!] Error: この監査スクリプトはroot権限で実行する必要があります。")
sys.exit(1)
def audit_config_file():
"""設定ファイルの存在とセキュリティパラメータを検証"""
print([*] 監査開始: 設定ファイルバリデーション -> {CONTAINERD_CONFIG_PATH})
if not os.path.exists(CONTAINERD_CONFIG_PATH):
print(f"[!] 警告: 設定ファイルが見つかりません: {CONTAINERD_CONFIG_PATH}")
return False
try:
with open(CONTAINERD_CONFIG_PATH, "r") as f:
config = toml.load(f)
# SystemdCgroupの設定確認
try:
cgroup_val = config["plugins"]["io.containerd.grpc.v1.cri"]["containerd"]["runtimes"]["runc"]["options"]["SystemdCgroup"]
if cgroup_val is True:
print("[+] OK: SystemdCgroup は有効に設定されています。")
else:
print("[-] 脆弱性検知: SystemdCgroup が false または未設定です。リソース分離が弱まる可能性があります。")
except KeyError:
print("[-] 脆弱性検知: SystemdCgroup の設定パスが見つかりません。デフォルトのままの可能性があります。")
except Exception as e:
print(f"[!] 設定ファイルの解析中にエラーが発生しました: {e}")
return False
return True
def audit_running_processes():
"""ホスト上で実行中の containerd / runc のプロセス引数を監査"""
print("\n[*] 監査開始: プロセス引数とデバッグモードの確認")
try:
result = subprocess.run(["ps", "-ef"], stdout=subprocess.PIPE, text=True, check=True)
processes = result.stdout.splitlines()
debug_found = False
for line in processes:
if "containerd" in line and "--log-level debug" in line:
debug_found = True
if debug_found:
print("[-] 警告: containerd がデバッグモードで稼働しています。機密情報がログに露出するリスクがあります。")
else:
print("[+] OK: containerd のログレベルは適切に制限されているか、デバッグモードではありません。")
except Exception as e:
print(f"[!] プロセス確認中にエラーが発生しました: {e}")
if __name__ == "__main__":
print("========================================")
print(" Containerd Security Hardening Auditor")
print("========================================")
check_root_privileges()
audit_config_file()
audit_running_processes()
print("\n[!] 監査が完了しました。警告が出力された項目は直ちに修正してください。")
このスクリプトを CI/CD パイプラインのインフラ構築ステップ(Ansible や Terraform の適用後)や、定期的な cron ジョブに組み込んでおくことで、構成ドリフト(Configuration Drift)によるセキュリティホールを防ぐことができる。
—
5. まとめ:セキュリティは「設定の引き算」から始まる
優れたインフラエンジニアは「いかに多くの機能やプラグインを動かすか」ではなく、「いかに不要な機能を削ぎ落とし、攻撃者の選択肢を奪うか」に心血を注ぐ。
コンテナランタイムの要塞化も全く同じだ。
containerd や CRI-O のデフォルト設定はあくまで「誰でも簡単に動かせるため」の妥協の産物であり、本番環境の盾としてはあまりにも脆い。今回紹介した設定や監査手法をベースに、自社の基盤を見直してみてほしい。
「動けばいい」という甘い汁を断ち切り、堅牢なランタイム基盤を作り上げる――それこそが、次のインシデントを防ぐ唯一にして最大の防御策なのだ。さて、自社のサーバーの設定ファイルを今すぐ確認しに行こうか。
コメント