背景:なぜLinux Capabilitiesを制限する必要があるのか?
深夜2時、SOCのアラームが突如鳴り響きました。サードパーティ製npmライブラリのRCE(リモートコード実行)脆弱性が突かれ、Node.jsウェブサービスにWebshellが設置されたのです。攻撃者は即座に内部ネットワークのスキャンを試み、ルーティングテーブルの改ざんやホストサーバーのパケットダンプを実行しようとしました。
幸いなことに、最悪のシナリオである「コンテナエスケープによるホストサーバーの乗っ取り」は発生しませんでした。理由は極めてシンプルで、このコンテナは起動時からカーネル操作権限が完全に剥奪されていたためです。
デフォルト設定において、Dockerはコンテナにホストの完全なroot権限を付与することはありません。しかし、それでも14種類のLinux Capabilities(CAP_NET_RAW、CAP_CHOWN、CAP_MKNOD、CAP_SYS_CHROOTなど)が標準で割り当てられています。
攻撃者がコンテナ内のroot権限を掌握した場合、これら不要なcapabilityを悪用しようとします。内部ネットワークのパケット盗聴やARPスプーフィングを実行したり、未パッチのカーネル脆弱性を突いてホストサーバーへの権限昇格を試みたりするリスクが生じます。
本番環境における鉄則は最小権限の原則(Least Privilege)です。最も安全なアプローチは、--cap-drop=ALLで全権限を一旦すべて剥奪した上で、サービスが真に必要とする1〜2個の権限のみを--cap-addで再付与することです。
Capabilities確認ツールの準備
コンテナが現在どの権限を保持しているかを正確に把握するため、Linuxマシンにlibcapツールスイートをインストールします。
Ubuntu/Debianでインストールコマンドを実行します:
sudo apt-get update && sudo apt-get install -y libcap2-bin libcap-ng-utils
それでは、テスト用Alpineコンテナを実行して、Dockerがデフォルトでどのような権限を付与しているか確認してみましょう:
# テスト用alpineコンテナを起動
docker run -d --name test-cap alpine sleep 3600
# ホスト上のコンテナプロセスのPIDを取得
PID=$(docker inspect --format '{{.State.Pid}}' test-cap)
# プロセスが保持しているcapabilitiesを確認
getpcaps $PID
# 確認後にクリーンアップ
docker rm -f test-cap
実行結果には、cap_chown、cap_net_raw、cap_sys_chrootなど多くの権限が表示されます。一般的なAPIマイクロサービスでは、CAP_NET_RAWやCAP_MKNODなどは一切不要です。
実践設定:cap-dropとcap-addの組み合わせ
本番運用で最も確実なルールはホワイトリスト方式です。つまり、「最初にすべてDropし、後から必要な権限だけをAddする」という原則です。
1. Docker CLIでの直接設定
起動時に80/443ポートのリッスンとファイル権限の設定のみが必要なNginxコンテナを例にとります。必要な権限はNET_BIND_SERVICE、SETUID、SETGIDの3つだけです:
docker run -d \
--name secure-web \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--cap-add=SETUID \
--cap-add=SETGID \
-p 80:80 \
nginx:alpine
権限を制限しておけば、仮に攻撃者がシェルを奪取したとしても、CAP_NET_RAWがないためpingやnmapによるネットワークスキャンを実行できません。また、CAP_MKNODもないため仮想ブロックデバイスを作成してホストのストレージにアクセスすることも不可能です。
2. Docker Composeでの設定標準化
多数のマイクロサービスを管理する場合、手動でのCLIコマンド実行ではセキュリティフラグの設定漏れが発生しやすくなります。すべての設定をdocker-compose.ymlファイルに直接定義して標準化しましょう:
version: '3.8'
services:
api-backend:
image: node:20-alpine
container_name: production-api
working_dir: /app
command: node server.js
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
ports:
- "80:80"
restart: always
no-new-privileges:trueの設定は極めて重要な防御壁です。sudoやsuidなど、setuid/setgidフラグを持つバイナリ経由で子プロセスが勝手に権限昇格するのを防ぎます。
主要なLinux Capabilities一覧表
- CAP_NET_BIND_SERVICE: 1024未満の特権ポート(80、443など)へのバインドを許可します。非rootユーザーで特権ポートを開くWebサーバーを実行する際に必須です。
- CAP_NET_RAW: Rawパケットの生成やICMPコマンド(ping)の実行に使用されます。ポートスキャンやARPスプーフィングを防ぐため、すべてのWeb/APIコンテナでDROPすることを推奨します。
- CAP_SYS_ADMIN: root権限の約90%に相当する強力な権限です。Docker-in-Dockerの実行やカーネルトレースなどの特殊なケースを除き、本番環境で有効にしてはいけません。
- CAP_CHOWN: ファイルの所有者UID/GIDの変更を許可します。エントリーポイントスクリプトで起動時にデータディレクトリの
chownが必要な場合にのみ付与します。
デプロイ後の権限確認とモニタリング
権限を制限した後は、コンテナが安定して動作しているか、そして意図通りに権限が剥奪されているかを検証する必要があります。
コンテナ内部からの直接権限確認
/proc/1/statusファイルからプロセスの状態を読み取ります:
docker exec -it secure-web sh -c "grep Cap /proc/1/status"
システムからCapabilityのビットマップを表す16進数文字列が出力されます:
CapInh: 0000000000000000
CapPrm: 00000000000004c0
CapEff: 00000000000004c0
CapBnd: 00000000000004c0
ホスト側でcapshコマンドを使用して、この16進数文字列をデコードします:
capsh --decode=00000000000004c0
画面にcap_setgid,cap_setuid,cap_net_bind_serviceと表示されます。不要なCapabilityが完全に削除されていることが確認できます。
Permission Deniedエラー発生時のトラブルシューティング
--cap-drop=ALLを追加した後にアプリケーションがクラッシュした場合でも、焦って安易に権限を戻してはいけません。コンテナログおよびホスト側のカーネル監査ログを確認しましょう:
# アプリケーションログを確認
docker logs --tail 50 secure-web
# カーネルによってブロックされたcapabilityを調査
sudo dmesg -T | grep -i "apparmor\|audit\|denied"
Operation not permittedエラーを確認したら、エラーの原因となっている処理を特定し、その処理に必要なCapabilityのみをピンポイントでcap_addに追加します。このルールを徹底することで、コンテナエスケープ攻撃からDockerインフラ全体を堅牢に保護できます。

