おい、最近のクラウドネイティブな開発現場を見ていると、どうも「動けば正義」の悪癖が抜けていない連中が多いようだ。「とりあえずデバッグのために」「マウントがどうこうエラーが出るから面倒くさい」という理由で、コンテナランタイムの Privileged(特権モード)を安易に有効化しているシステムに何度も遭遇する。
レッドチームとして言わせてもらうが、Privileged: true で動いているコンテナを見つけたら、それは我々にとって「どうぞホストOSのシェルを奪ってください」と言っているようなものだ。コンテナの壁なんて、特権の前では紙くず同然。今回は、その特権モードがいかに危険かという現実と、それを完全に封殺するための実践的なセキュリティ対策を叩き込んでやる。
—
なぜ Privileged コンテナは「ホスト直結」なのか
DockerやKubernetesにおいて、コンテナはネームスペース(Namespace)とcgroupsによって隔離されている。プロセス、ネットワーク、ファイルシステムあたりの視界が制限されているから、コンテナ内で暴れても影響は内部で完結する……というのが教科書的な建前だ。
しかし、SecurityContext で privileged: true を明示、あるいは誤って設定してしまうと、この隔離壁の大部分が無効化される。具体的に何が起きるかというと:
1. すべてのデバイスドライバへのアクセス許可: ホスト側の /dev にある物理デバイスやGPU、ストレージデバイスに直接触れるようになる。
2. カーネル機能(Capabilities)の全解放: 通常のコンテナでは制限されている CAP_SYS_ADMIN などの強大な権限がすべて付与される。
3. AppArmorやSELinuxの制限無効化: マンダトリーアクセス制御(MAC)のポリシーがバイパスされる。
要するに、「ホストのroot権限」をコンテナの内部から直接行使できる状態になる。これを利用したコンテナ脱獄(Container Escape)の典型的な手口を見ていこう。
—
恐怖の脱獄:特権コンテナからのホスト侵入(PoCの裏側)
攻撃者は特権コンテナのシェルを奪った後、ホストOSへジャンプするためにいくつかの常套手段を使う。最も古典的かつ確実なのが、ホスト側のデバイスファイルを利用したファイルシステム直叩きだ。
例えば、コンテナ内で以下のコマンドを実行されたとする。
# コンテナ内からホストのディスクデバイスを特定する
fdisk -l
# ホストのルートファイルシステムを適当なディレクトリにマウント
mkdir /tmp/host_root
mount /dev/sda1 /tmp/host_root
# ホスト側のSSH公開鍵を書き換えて永続化完了
echo "ssh-rsa AAAAB3NzaC1yc2E..." >> /tmp/host_root/root/.ssh/authorized_keys
たったこれだけだ。コンテナの中からホストのブロックデバイスを直接マウントし、ホスト側の /etc/shadow を書き換えたり、SSHのバックドアを仕込んだりすることがいとも簡単にできてしまう。ネットワーク越しの脆弱性を突くまでもなく、設定ミス一つでインフラ全体の陥落が完了するのだ。
—
開発現場でやるべき「完全防御」の実装ルール
後輩の君たちに求むのは、「なぜ特権が必要なのか」を疑うエンジニアリングの姿勢だ。大半のケースにおいて、アプリが Privileged を要求する理由は設計ミスか、適切な Capabilities の付与をサボっている怠慢のどちらかである。
ここからは、Kubernetesの SecurityContext を用いて、最小権限の原則(Principle of Least Privilege)を厳格に適用するための具体的な設定ファイルとコード例を解説する。
1. Kubernetesマニフェストでの厳格なセキュリティコンテキスト
KubernetesでPodをデプロイする際は、Podレベルおよびコンテナレベルで必ず以下のように設定し、特権の昇格や不要な機能を完全に削ぎ落とせ。
apiVersion: v1
kind: Pod
metadata:
name: secure-production-pod
namespace: default
spec:
# Pod全体でセキュリティベースラインを強制
securityContext:
runAsNonRoot: true # rootユーザーでの実行を禁止
runAsUser: 10001 # 実行ユーザーIDを固定
runAsGroup: 10001 # 実行グループIDを固定
fsGroup: 10001 # ボリュームのファイルシステムグループ
seccompProfile:
type: RuntimeDefault # デフォルトのシッコンププロファイルでシステムコールを制限
containers:
- name: app-container
image: my-secure-app:latest
securityContext:
privileged: false # 【最重要】特権モードの明示的な無効化
allowPrivilegeEscalation: false # 特権昇格(sudo等)の防止
readOnlyRootFilesystem: true # ルートファイルシステムを読み取り専用にする
capabilities:
drop:
- ALL # すべてのLinux機能を一旦剥奪する
# 本当に必要な最小限の機能だけを個別に許可する場合のみ追加(例: ネットワークバインドのみ)
# add:
# - NET_BIND_SERVICE
# 書き込みが必要な一時ディレクトリなどは明示的にvolumeMountsで切り出す
volumeMounts:
- mountPath: /tmp
name: tmp-dir
volumes:
- name: tmp-dir
emptyDir: {}
2. アプリケーション層(Python)でのファイルシステム保護
インフラ側だけでなく、アプリケーション層でも「コンテナのファイルシステムは基本的に書き込めない(あるいは書き込み先が制限されている)」という前提でコードを書く必要がある。例えば、Pythonでアップロードファイルを処理する際の実装を見てみよう。
import os
from pathlib import Path
from fastapi import FastAPI, File, HTTPException, UploadFile
app = FastAPI()
# 読み取り専用ファイルシステム環境下では、書き込みは許可された一時領域のみに行う
UPLOAD_DIR = Path("/tmp/uploads")
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
@app.post("/api/v1/upload")
async def upload_file(file: UploadFile = File(...)):
# パストラバーサル攻撃を防ぐためのサニタイジング
safe_filename = Path(file.filename).name
destination = UPLOAD_DIR / safe_filename
try:
# チャンク単位で書き込み、メモリ枯渇とディスク圧迫を防ぐ
with destination.open("wb") as buffer:
while chunk := await file.read(1024 * 1024): # 1MBごとに処理
buffer.write(chunk)
except IOError as e:
# 読み取り専用領域への書き込み試行などのエラーをハンドリング
raise HTTPException(
status_code=500, detail="ストレージへの書き込みに失敗しました。"
) from e
return {
"status": "success",
"filename": safe_filename,
"message": "正常にアップロードされました。",
}
このPythonコードでは、ルート領域への書き込みを一切行わず、あらかじめ許可された /tmp 配下に処理を限定している。インフラ側の readOnlyRootFilesystem: true と組み合わせることで、万が一Webアプリケーションに任意ファイル書き込みの脆弱性が存在したとしても、システム全体への致命的なダメージを防ぐ多層防御が成立する。
—
セキュリティチーフからの総括
セキュリティは「動くものを作る」ことの対極にある面倒な作業に見えるかもしれない。だが、インシデントが発生した瞬間に、これまでの開発スピードは何倍もの負債となって会社に跳ね返ってくる。
「便利だから」「よく分からないけど動くから」という理由で Privileged: true を書く手は今すぐ止めろ。コンテナの境界線を守ることは、君たちが作り上げるシステムの信頼を守ることに直結している。明日からのコードレビューでは、この設定漏れを見逃さないように厳しくチェックしていくこと。頼んだぞ。
コメント