コンテナの「root=無敵」という幻想を叩き壊せ:Capabilities最小化とDrop Allの実際
おい、最近のコンテナ設計を見ていると、どうも「DockerやKubernetesを使っていれば、それだけでセキュリティが担保される」という甘い勘違いをしている連中が多い。特に、アプリケーションのビルドや動的ライブラリのロードでエラーが出ると、思考停止でコンテナに privileged: true をぶち込んだり、不要な特権を与えたまま本番稼働させたりするケースが後を絶たない。
インシデントレスポンスの現場に立ってみろ。コンテナエスケープを許した瞬間、ホストOSのカーネルは敵の手に落ちる。ホスト側の /var/run/docker.sock が叩かれれば、その物理・仮想基盤上のすべてのテナントが蹂躙されるのだ。
今回は、Linuxカーネルが持つ強力な武器である Capabilities(機能) に焦点を当て、特に「すべての権限を剥奪してから必要なものだけを最小限で与える(Drop All Capabilities)」という、現場で即座に実践すべき要塞化の手法を徹底的に叩き込んでやる。
—
1. 攻撃者の視点:なぜ CAP_SYS_ADMIN がコンテナの命取りになるのか
Linuxカーネルは、昔ながらの「root(UID 0)なら何でもできる」という大雑把な権限管理を、細分化された権限の単位である Capabilities へと分割した。これにより、プロセスは「ファイル所有者の変更(CAP_CHOWN)」や「ネットワークバインド(CAP_NET_BIND_SERVICE)」といった特定の特権だけを行使できるようになった。
だが、ここで問題になるのが、Dockerのデフォルト設定や誤った設定だ。コンテナランタイムがデフォルトで付与するCapabilitiesの中には、攻撃者にホストエスケープの足がかりを与えてしまう危険なものが含まれている。
危険なCapabilitiesの代表格:CAP_SYS_ADMIN
CAP_SYS_ADMIN は、いわば「何でも屋」の特権だ。マウント操作、ネームスペースの操作、デバイスの制御など、実に多様なシステム管理コマンドがこの権限一つで実行できてしまう。
もし、Webアプリケーションに任意のファイル書き込み脆弱性やコンテナ内でのコマンド実行(RCE)の脆弱性が存在し、かつコンテナに CAP_SYS_ADMIN が付与されていた場合、攻撃者は以下のような手順でコンテナからホストOSへと脱獄(エスケープ)を果たす。
1. ホストファイルシステムの再マウント:
コンテナ内からホスト側のブロックデバイスや、ホストの根幹ファイルシステム(/proc や /sys を経由した領域)を不正にマウントする。
2. ホストのcronやssh設定の書き換え:
マウントしたホスト側の領域にある /etc/crontab や /root/.ssh/authorized_keys にバックドアを仕込む。
3. ホスト上の特権プロセスの乗っ取り:
ホスト側で実行されるデーモンを書き換え、完全にホストの根幹を掌握する。
「うちは非特権ユーザー(non-root user)でアプリケーションを動かしているから大丈夫だ」という言い訳は通用しない。ファイルシステムの構造やカーネルの機能不備をつく攻撃の前では、UIDが何番であるかなど気休めにもならないのだ。
—
2. 徹底的な要塞化:Drop All Capabilities の設計思想
現場で取るべき対策はシンプルかつ厳格だ。「最初にすべてのCapabilitiesを剥奪し(Drop All)、アプリケーションの稼働にどうしても必要な最小限の権限だけをホワイトリスト方式で再付与する」。これが鉄則だ。
Docker ComposeやKubernetesのマニフェストでは、この「Drop All & Add Required」を宣言的に定義できる。
Docker Composeでの実装例
以下の設定ファイルを見てほしい。これは、Node.jsやPythonなどで構築された通常のWebアプリケーションコンテナを想定し、すべてのCapabilitiesを剥奪した上で、特権を一切持たないセキュアな状態を作り上げる設定だ。
version: '3.8'
services:
web_app:
image: my-secure-app:latest
restart: always
# コンテナ内のプロセスを非特権ユーザーで実行
user: "10001:10001"
# 読み取り専用ルートファイルシステムの設定(必要に応じて一時ディレクトリをtmpfsでマウント)
read_only: true
volumes:
- tmp_data:/app/tmp
# セキュリティオプショントの適用
security_opt:
- no-new-privileges:true
# 【最重要】すべてのCapabilitiesを剥奪し、必要なものだけを厳選して追加
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # ポート80や443など1024番未満のポートにバインドする場合のみ追加。不要ならこれも削れ。
environment:
- NODE_ENV=production
volumes:
tmp_data:
この設定がもたらす防御効果
cap_drop: [ALL]: カーネルが持つすべての特権能力を完全に剥奪する。これにより、万が一コンテナが破られても、マウント操作やシステム時刻の変更、ネットワークインターフェースの改変などは一切不可能になる。security_opt: [no-new-privileges:true]:setuidやsetgidビットが付いたバイアンスを用いた権限昇格の試みをカーネルレベルでブロックする。read_only: true: アプリケーションのコードベースが配置された領域をイミュータブル(変更不能)にし、Webシェルなどのマルウェアが勝手にファイルを書き込む余地を断つ。
—
3. 実務での検証:アプリケーションコードとインフラ側の連携
では、このような極限まで絞り込んだ環境で、実際のWebアプリケーション(例:Python / Flask)はどう動作すべきか。特権を剥奪された環境では、root権限を前提とした処理(特権ポートへのバインド、システム設定の変更など)がエラーを引き起こす。
以下のPythonコードは、Capabilitiesを最小化した環境で安全に動作するサーバー起動の実装例だ。1024番未満のポートを使う代わりにリバースプロキシを前段に置く設計にしている。
#!/usr/bin/env python3
"""
セキュアなWebアプリケーションの実装サンプル
コンテナのCapabilities最小化(Drop All)および非特権実行(UID 10001)を前提とする。
"""
import os
import sys
from flask import Flask
app = Flask(__name__)
@app.route("/healthz")
def health_check():
"""ロードバランサーやK8sのlivenessProbe用エンドポイント"""
return {"status": "healthy", "uid": os.getuid()}, 200
@app.route("/")
def index():
return "Secure Application is running with minimal capabilities.", 200
if __name__ == "__main__":
# 警告: 特権を剥奪されたコンテナ内でポート80や443を開こうとすると
# CAP_NET_BIND_SERVICE がない場合、PermissionErrorが発生する。
# 実務では、NginxやEnvoyなどのリバースプロキシを前段に置き、
# アプリケーションは1024番以上の高位ポート(例: 8080)で待ち受けるのが鉄則。
port = int(os.environ.get("APP_PORT", 8080))
if port < 1024:
print("[ERROR] 1024番未満のポートへの直接バインドは禁止されています。", file=sys.stderr)
print("[ERROR] リバースプロキシを介した構成に変更してください。", file=sys.stderr)
sys.exit(1)
print(f"[*] Starting secure application on port {port} as UID {os.getuid()}...")
# デバッグモードは本番環境では絶対にオフにすること
app.run(host="0.0.0.0", port=port, debug=False)
—
4. Kubernetes(K8s)環境での実装アプローチ
Dockerだけでなく、本番のオーケストレーションツールであるKubernetesでも同様のポリシーを徹底しなければ意味がない。Pod Security Standards(PSS)の Restricted プロファイルに準拠し、セキュリティコンテキスト(securityContext)を次のように厳格に定義する。
apiVersion: v1
kind: Pod
metadata:
name: secure-backend-pod
namespace: production
spec:
containers:
- name: api-server
image: my-secure-app:latest
securityContext:
# rootユーザーでの実行を禁止
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
# ルートファイルシステムを読み取り専用に
readOnlyRootFilesystem: true
capabilities:
# すべての権限をドロップ
drop:
- ALL
# 必要最小限の権限のみを付与(この例では完全ゼロ)
add: []
volumeMounts:
- mountPath: /app/tmp
name: tmp-dir
volumes:
- name: tmp-dir
emptyDir: {}
ここまで徹底して初めて、「我が社のコンテナ基盤は堅牢である」と胸を張って言える。
—
5. まとめ:甘えを捨てたインフラ設計を
セキュリティの現場において、「動けばいいや」という妥協は最大の脆弱性だ。
コンテナのCapabilitiesの最小化は、面倒な設定に見えるかもしれないが、万が一アプリケーションにゼロデイ脆弱性や重大なインジェクションが発見された際、攻撃者の被害範囲を「そのコンテナ内での一時的なコード実行」に完全に封じ込めるための最強の防壁となる。
後輩エンジニアの指導にあたるときも、この原則を徹底してほしい。「なぜこの権限が必要なのか?」を説明できない設定は、すべて悪である。今日から君たちのプロジェクトでも、cap_drop: [ALL] をデフォルトのスタンダードとして導入せよ。
コメント