おい、ちょっと手を止めてくれ。昨日、ステージング環境で見つかったあのインシデント、覚えているか?
ある開発者がテスト用にデプロイしたコンテナ内のWebアプリに脆弱性(RCE:リモートコード実行)があってな、そこからいとも簡単にコンテナの脱出(Container Escape)を試みられ、ホストの /etc/shadow を覗き見られそうになった件だ。
「いやぁ、コンテナ使ってるからプロセスは分離されてるし、root権限でも大丈夫っしょ」なんて甘い考えを持っているなら、今すぐその認識を改めろ。コンテナは「魔法の箱」じゃない。ただのLinuxカーネルの機能(名前空間とcgroups)を組み合わせた、いわば「緩い檻」にすぎない。檻の鍵が壊れれば、中の猛獣(攻撃者)は簡単に外に出てくる。
今回は、その檻を文字通り「鉄格子」にするための技術、AppArmor / SELinuxによる強制アクセス制御(MAC: Mandatory Access Control)について、現場の泥臭い知見を交えて徹底的に解説する。
—
なぜデフォルトのDockerでは不十分なのか?
DockerやKubernetesを導入しただけで「セキュアになった」と胸を張るエンジニアが多いが、それは大きな間違いだ。デフォルトのランタイム設定では、DAC(任意アクセス制御)の範疇を出ない。
例えば、コンテナ内で何らかの脆弱性を突かれ、シェルを取られたとしよう。もしそのコンテナがデフォルトのAppArmorプロファイル(docker-default)で動いていれば、ある程度システムコールやファイルへの書き込みは制限されるが、「読めてはいけない機密ファイルへのアクセス」や「想定外のネットワーク接続」を完全に防ぎきれるわけではない。
特に、Kubernetes環境などでセキュリティコンテキスト(securityContext)の設定をサボっている現場を見ると、私は夜も眠れなくなる。攻撃者はコンテナを踏み台にして、ホストのソケット(/var/run/docker.sock)を叩き、兄弟コンテナを次々と乗っ取る。これが実戦で最もよく使われる王道のラテラルムーブメント(水平移動)だ。
この悪夢を断ち切る唯一の盾が、カーネルレベルの強制アクセス制御、すなわちSELinuxやAppArmorによるプロファイル適用なのだ。
—
攻撃者の視点:RCEからホスト制圧までのシナリオ
まずは、敵を知るために、よくある脆弱なPython製Webアプリをコンテナで動かしているシーンを想像してほしい。このアプリには、ユーザー入力をそのまま subprocess.Popen に渡してしまうという、教科書通りの致命的なRCE脆弱性があるとしよう。
脆弱なアプリケーションのサンプル(Python)
# 決して真似してはいけない脆弱なコード例
import subprocess
from flask import Flask, request
app = Flask(__name__)
@app.route("/exec")
def execute_command():
# ユーザーからの入力をそのままOSコマンドとして実行してしまう
user_cmd = request.args.get("cmd", "echo 'Hello'")
try:
# 脆弱性:任意のコマンドがコンテナ内で実行可能
output = subprocess.check_output(
user_cmd, shell=True, stderr=subprocess.STDOUT, timeout=5
)
return f"<pre>{output.decode('utf-8')}</pre>"
except Exception as e:
return f"<pre>Error: {str(e)}</pre>"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
攻撃者がこのエンドポイントに対し、以下のようなリクエストを送り込んだとする。
http://vulnerable-app/exec?cmd=cat+/etc/shadow
もしコンテナの権限設定がガバガバであれば、運悪く(いや必然的に)パスワードハッシュのリストが画面に晒されることになる。さらに、ここからカーネルの脆弱性(CVE)を組み合わせてコンテナ脱出を図られれば、ホストOSは完全に乗っ取られる。
—
AppArmorプロファイルによる「鉄壁の封じ込め」
このリスクを根絶するためには、「このコンテナプロセスは、指定されたファイル以外への読み書きを一切行ってはならない」という厳格なルール(プロファイル)をカーネルに強制する必要がある。
ここでは、UbuntuなどのDebian系で標準的に使われる AppArmor を用いて、特定のコンテナがアクセスできる範囲を極限まで絞り込む実務的な設定手順を見ていこう。
1. カスタムAppArmorプロファイルの作成
ホスト側の /etc/apparmor.d/docker-custom-secureapp として、以下のプロファイル定義を配置する。ここでは、Webアプリが稼働するコンテナに対し、必要最低限のファイル読み取りと、ログ出力先以外の書き込みを一切禁止するルールを定義している。
#include <tunables/global>
# コンテナ用のカスタム強制アクセス制御プロファイル
profile docker-custom-secureapp flags=(attach_disconnected,mediate_deleted) {
# デフォルトの基本的なルールをインクルード
#include <abstractions/base>
network,
capability net_bind_service,
# /app ディレクトリ以下の実行と読み取りのみを許可
r /app/,
rk /app/**,
# ログ出力先など、最低限必要な一時ディレクトリへの書き込み許可
w /tmp/app_cache/,
w /tmp/app_cache/**,
# 【重要】攻撃者が絶対に覗き見してはいけない機密ファイルへのアクセスを明示的に拒否
deny /etc/shadow r,
deny /etc/passwd r,
deny /root/** rw,
deny /proc/sys/** w,
# その他、システム全体の重要なファイルやデバイスへのアクセスを遮断
deny /sys/** w,
}
2. プロファイルのロード
作成したプロファイルファイルをホストのAppArmorカーネルモジュールに読み込ませる。
# プロファイルをパーサに通してカーネルにロードする
sudo apparmor_parser -r -W /etc/apparmor.d/docker-custom-secureapp
3. Dockerランタイムでのプロファイル適用
コンテナを起動する際、あるいはKubernetesのマニフェストを書く際に、作成したカスタムプロファイルの名前を指定してランタイムに強制する。
Docker CLIで起動する場合は、以下のように --security-opt オプションを指定する。
# AppArmorプロファイルを指定してコンテナをセキュアに起動
docker run -d \
--name secure-web-app \
--security-opt apparmor:docker-custom-secureapp \
-p 5000:5000 \
my-python-app:latest
もし、先ほどの脆弱なエンドポイントから攻撃者が cat /etc/shadow を実行しようとしても、AppArmorがカーネルレベルでそのシステムコールをインターセプトし、「Permission denied(権限がありません)」として容赦なく弾き返す。例えコンテナ内でroot権限を奪われていたとしても、ホストや他領域のファイルには指一本触れられないというわけだ。
—
SELinux(MCS/MLS)を活用したマルチテナント分離
Red Hat Enterprise Linux (RHEL)やCentOS、Amazon Linuxなどの環境では、SELinuxがデフォルトの強制アクセス制御システムとして君臨している。SELinuxの場合、コンテナごとに固有のセキュリティコンテキスト(ラベル)を付与し、プロセスとリソースの結びつきを厳密にコントロールする。
DockerやPodmanでSELinuxを有効にする場合、コンテナ起動時に :z や :Z オプションを使ってボリュームマウントのラベルを適切に管理することが、現場でのインシデントを防ぐ第一歩だ。
SELinuxを意識したDocker Composeの鉄板設定
version: '3.8'
services:
web:
image: my-python-app:latest
container_name: secure_selinux_app
# SELinuxのコンテナ用カテゴリを適切に分離・適用
security_opt:
- label:type:container_t
- label:level:s0:c100,c200
volumes:
# :z は複数のコンテナ間でボリュームを共有する場合、
# :Z は特定のコンテナ専用にプライベートなラベルを付与する場合に使い分ける
- ./app_data:/app/data:Z
restart: always
現場のジュニアエンジニアによくあるミスが、面倒くさいからといってホスト側で setenforce 0(Permissiveモードへの移行)や、コンテナ側で --privileged を安易に付けてしまうことだ。
「動かないからセキュリティを緩める」というのは、セキュリティチーフとしては絶対に許されない怠慢だと言っておこう。動かない原因は、アクセス権限やSELinuxのコンバート設定(ブール値など)が間違っているからであり、セキュリティを解除するのではなく、正しく監査ログ(/var/log/audit/audit.log)を読み解いてポリシーを修正するのがプロの仕事だ。
—
現場のエンジニアへ贈るセキュリティの極意
コンテナのハーデニングは、一度設定して終わりではない。アプリケーションの仕様変更に伴い、必要なファイルやネットワークポートが変われば、AppArmorやSELinuxのプロファイルも追従させる必要がある。
最後に、インシデントを未然に防ぐためのチェックリストを頭に叩き込んでおいてくれ。
1. rootでコンテナを動かさない:Dockerfile内で必ず専用の非特権ユーザー(USER appuser など)を作成し、その権限でプロセスを走らせる。
2. デフォルトプロファイルを過信しない:Docker標準の制限に加え、今回紹介したカスタムAppArmor/SELinuxプロファイルで「攻撃者の行動範囲」を物理的に狭める。
3. 不要な特権・ケーパビリティを剥ぎ取る:--cap-drop=ALL を基本とし、アプリの動作に最低限必要なケーパビリティ(例: NET_BIND_SERVICE)のみをホワイトリスト形式で --cap-add する。
4. 監査ログを監視する:AppArmorのコンパイル時に complain モード(監査モード)を活用して挙動をテストし、本番では必ず enforce モードでブロックを有効化する。
セキュリティとは、技術の足し算ではなく、隙間を埋める引き算の美学だ。不要な権限を削ぎ落とし、コンテナを真の「堅牢な要塞」へと仕立て上げよう。チーム全体のセキュリティ意識の底上げを、君たちの手で頼む。
コメント