【実務・中級編】 コンテナのユーザー名前空間(User Namespaces)の有効化 – インフラ・ネットワーク & クラウドセキュリティ防御ガイド

おい、そこの君。ちょっと手を止めて画面を見てくれ。

先週、別のチームが管轄しているステージング環境の踏み台サーバーから、不審な挙動が検知された件、覚えているか? 侵入経路を洗ったところ、Webアプリケーション経由でコンテナの脆弱性が突かれ、最終的にホストOSの root 権限が奪われていたんだ。

「えっ、コンテナってちゃんと隔離されているんじゃないんですか?」
そう驚く顔をしているな。だが、インフラエンジニアや開発者が陥りがちな最大の勘違いがそれだ。「コンテナ=完全な仮想マシン」という幻想を抱いていること。

デフォルトのDockerコンテナは、ホストOSとカーネルを共有している。そして、最も恐ろしいことに、コンテナ内の root ユーザー(UID 0)は、マッピングの設定をしていなければ、ホストOS側の root ユーザーと完全に一致しているんだ。

もしコンテナが脱獄(コンテナエスケープ)されたり、アプリケーションの脆弱性からRCE(リモートコード実行)を許したりしたらどうなるか? ホストの root を奪われた瞬間、同じホスト上で動いている他のコンテナや、最悪の場合はクラウド基盤のメタデータサービスまで丸裸になる。

今回は、この悪夢のようなシナリオを根底から断ち切るための技術、「ユーザー名前空間(User Namespaces)の有効化」について、現場の泥臭い知見を交えて徹底的に解説する。理論だけじゃない、今すぐプロダクション環境に適用できる設定ファイルを渡すから、しっかりと頭と手を動かしてくれ。

—

1. 攻撃者が狙う盲点:なぜコンテナの root は危険なのか

まずは、敵が何を考えているのか、その手口を理解しよう。

多くの開発者は、Dockerfile内でわざわざ USER 1000 などの非特権ユーザーを指定せず、うっかりデフォルトのまま root でアプリケーションを動かしている。
「どうせコンテナの中だけの権限だし、外からは隔離されているから大丈夫だろう」と。

だが、甘い。セキュリティの現場では、その「油断」が命取りになる。

脆弱性の連鎖とコンテナエスケープのPoC(概念実証)

例えば、コンテナ内で動くWebアプリケーションに任意のファイル読み出し、あるいはカーネルの脆弱性(古いruncの脆弱性など)を突く足がかりがあったとする。

攻撃者はコンテナ内で次のようなコマンドを実行し、ホスト側のリソースにアクセスを試みる。

# コンテナ内での実行を想定
# UIDを確認すると、当然「0(root)」で実行されている
$ id
uid=0(root) gid=0(root) groups=0(root)...

# もしホストの /proc やデバイスファイルがマウントミスなどで露出していれば...
# ホスト側の重要ファイルやプロセス空間にアクセスが可能になってしまう
$ ls -la /host-root/etc/shadow

もし、ユーザー名前空間が無効な環境でコンテナエスケープが発生した場合、攻撃者はホストOS上でもそのまま root 権限を手に入れたことになる。ホスト上の全コンテナのデータ盗聴、暗号通貨のマイニングツールの勝手なデプロイ、さらにはクラウド環境のIAMロールを悪用した水平展開(Lateral Movement)へと進む。

これを防ぐ唯一にして最強の盾が、ユーザー名前空間(User Namespaces / userns)だ。

—

2. ユーザー名前空間(User Namespaces)の仕組み

ユーザー名前空間とは、Linuxカーネルの機能の一つで、「コンテナ内のユーザーID(UID)およびグループID(GID)を、ホストOS上の全く別の(権限を持たない)UID/GIDにマッピング(変換)する技術」だ。

これを発動させると、どうなるか?

  • コンテナ内から見た世界: アプリケーションは自分が root(UID 0)であると錯覚して元気に動く。
  • ホストOSから見た世界: そのプロセスは、ホスト上ではただの無名な一般ユーザー(例えば UID 100000 など)として扱われる。

仮に、コンテナから脱獄してホストOS側にコードの足跡を残したとしても、ホスト側には何の権限もない。ホストの root ファイルを書き換えることは物理的(論理的)に不可能になる。これが、ゼロトラストアーキテクチャにおける「多層防御」の基本だ。

—

3. 実装:Docker daemonでユーザー名前空間を強制する

口で言うのは簡単だ。では、実際にどうやって設定するのか。
開発環境や検証環境ではなく、厳格なセキュリティが求められる本番(プロダクション)のLinuxサーバー(Ubuntu/RHEL等)を想定して手順を示す。

ステップ 1: デーモン設定ファイル(daemon.json)の修正

Dockerの全体設定である /etc/docker/daemon.json を開き、ユーザー名前空間のマッピングを有効にする。既存の設定がある場合はJSONの構文ミスに注意して追記してくれ。

{
  "userns-remap": "default"
}

> 【プロのワンポイントアドバイス】
> "default" を指定すると、Dockerは自動的に dockremap という専用のシステムユーザーとグループを生成し、そのユーザー空間(通常は65536個のUID/GID範囲)をすべてのコンテナに割り当てる。特定のユーザーやグループを明示的に指定したい場合は、"userns-remap": "myuser:mygroup" のように書くことも可能だ。

ステップ 2: Dockerサービスの再起動

設定を反映させるために、デーモンを再起動する。

# systemd経由でDockerを再起動
$ sudo systemctl restart docker

これで完了…と言いたいところだが、ここからがインフラエンジニアの腕の見せ所だ。

—

4. 現場で絶対にハマる「落とし穴」と回避策

ユーザー名前空間を有効にした瞬間、既存のアプリケーションやCI/CDパイプラインが盛大に壊れることがある。現場でよくあるトラブルと、その対策を共有しておこう。

トラブル 1: ボリュームマウント(-v や --mount)したファイルのパーミッションエラー

ホスト側のディレクトリをコンテナ内にマウントしている場合、ホスト側では root(UID 0)が所有者であるファイルが、コンテナ内(ユーザー名前空間内)からは別人の所有者に見えたり、書き込み権限が消失したりして、アプリが起動時にクラッシュする。

【対策】

アプリケーションが書き込む必要のあるストレージ(ログやアップロードディレクトリなど)は、名前空間のオフセットを考慮した適切なパーミッション設定を行うか、コンテナ起動時に動的に所有者を変更するエントリポイントスクリプトを用意する。

以下は、Python製アプリケーションのコンテナを安全に起動するための、実用的な entrypoint.sh のサンプルだ。

#!/usr/bin/env bash
set -euo pipefail

# コンテナ内で実行されるため、ここでの root は名前空間内の root(UID 0)
# ホスト上では非特権ユーザーにマッピングされている

LOG_DIR="/app/logs"
DATA_DIR="/app/storage"

# ディレクトリが存在しない場合は作成
mkdir -p "${LOG_DIR}" "${DATA_DIR}"

# 権限の調整(必要に応じてアプリケーション実行ユーザーに合わせる)
echo "[*] Initializing volume permissions..."
chown -R appuser:appuser "${LOG_DIR}" "${DATA_DIR}"

# ログに出力してフォアグラウンドでアプリケーションを安全に起動
echo "[*] Starting application service..."
exec python -u /app/main.py

トラブル 2: 特権コンテナ(--privileged)との併用不可

ユーザー名前空間を有効化すると、--privileged フラグを使ったコンテナや、ホストのネットワーク名前空間を直接共有するコンテナ(--net=host)の一部機能が制限される、あるいは起動エラーになる。

【対策】

「なんとなく便利だから」という理由で --privileged を使っているコードは、今すぐリファクタリングの対象にしろ。どうしても必要なデバイスアクセスがある場合は、--device フラグを個別に指定して最小限の権限だけを渡す設計に改めること。

—

5. 動作確認:本当に隔離されているか検証する(PoC)

設定を変えたら、必ず「意図通りに動いているか」を自分の手で検証(テスト)するのがプロの流儀だ。祈るな、検証しろ。

次のような簡単なPythonスクリプトを用意し、コンテナ内とホストOSの両方で実行してみよう。

#!/usr/bin/env python3
import os

def check_identity():
    # 現在のプロセスの実UIDおよび実GIDを取得
    uid = os.getuid()
    gid = os.getgid()
    
    print(f"--- ユーザーアイデンティティ検証 ---")
    print(f"現在の 実UID (Real UID): {uid}")
    print(f"現在の 実GID (Real GID): {gid}")
    
    if uid == 0:
        print("[!] 警告: このコンテナ内では root として実行されています。")
        print("    しかし、ユーザー名前空間が有効であれば、ホスト上では一般ユーザーにマッピングされています。")
    else:
        print("[+] 安全です: コンテナ内でも非特権ユーザーとして動作しています。")

if __name__ == "__main__":
    check_identity()

このスクリプトをコンテナ内で実行した際、コンテナ内では uid = 0 と表示されるのに対し、ホストOS側で ps aux や ls -n を使って該当プロセスの実効UIDを調べると、100000 や dockremap に割り当てられた別のIDに変換されていることが確認できるはずだ。これが、正しくハーデニングされた証拠となる。

—

最後に:セキュリティは「面倒くさい」の積み重ねの先にある

今回紹介したユーザー名前空間の有効化は、設定ファイル数行の変更で完了する。しかし、既存のボリュームマウントの設計見直しなど、少しばかりの手間がかかるのは事実だ。

だが、思い出してほしい。たった一度のインシデントで、会社の信用、顧客のデータ、そして君たちが丹精込めて書いたプロダクトコードがすべて灰と帰すリスクに比べれば、この「ひと手間」など安いものだ。

「動けばいいや」の時代は終わった。
明日から、いや今日から、君のチームのインフラストラクチャを一段上の堅牢さに引き上げてくれ。頼んだぞ。

コメント

タイトルとURLをコピーしました