言語を切り替える
テーマを切り替える

Docker安全認証:コンテナをrootで実行しないための完全実践ガイド

先日、友人の会社のコンテナ設定を手伝ったとき、何気なく docker inspect を叩いて驚きました。すべてrootで動いており、しかも --privileged が付いていたのです。私が「これがいかに危険か知ってる?」と尋ねると、彼は肩をすくめて「動けばいいんだよ、セキュリティは後回し」と言いました。その2週間後、彼らのコンテナは攻撃され、コンテナ脱出(Container Escape)によってホストマシンごと乗っ取られました。

これは脅しではありません。2024年1月に発覚したCVE-2024-21626脆弱性は典型例です。攻撃者はコンテナの「作業ディレクトリ」パラメータを操作するだけで、ファイル記述子を漏れさせ、ホストのファイルシステムを自由に操作できました。さらに恐ろしいデータがあります。緑盟科技(NSFOCUS)の研究によると、Docker Hub上の画像の76%にセキュリティ脆弱性が含まれており、67%には高リスクな脆弱性があるとのことです。

正直、私も以前はあまり気にしていませんでした。Dockerfileにはお決まりのように FROM ubuntu と書き、RUN apt-get install していました。「コンテナの中なんだから隔離されてるでしょ?」と。しかし、負荷テスト環境のコンテナが浸入され、ログにハッカーがホストのディスクをマウントするコマンドが見えたとき、背筋が凍りました。

実は、コンテナを非rootユーザーで動かすのは、思っているほど難しくありません。今回は、なぜデフォルトのrootが危険なのか、Dockerfileでの非rootユーザー作成方法、--user パラメータの使い方、そして Capabilities や AppArmor といった高度な設定までを解説します。これを読めば、本番環境のコンテナのリスクを少なくとも80%は低減できるはずです。

76%
脆弱性あり
Docker Hub統計
67%
高リスク脆弱性
セキュリティ研究報告
80%
リスク低減
非root設定による効果
数据来源: NSFOCUS Research Report

なぜrootでコンテナを動かしてはいけないのか?

コンテナ脱出:サンドボックスからホストへは一歩の距離

多くの人は「コンテナ=サンドボックス」で、中で何をしてもホストには影響しないと思っています。しかし実際には、コンテナの隔離はLinuxの namespace と cgroup に依存しており、仮想マシンのようなハードウェアレベルの隔離ではありません。設定ミスやカーネルの脆弱性があれば、この隔離壁は紙切れ同然になります。

CVE-2024-21626 は良い教訓です。攻撃者は runc(Dockerの低レベルランタイム)の脆弱性を突き、ホストファイルシステムへのアクセス権を手に入れました。つまり、コンテナ内からホスト上の /usr/bin/bash を書き換えることも可能だったのです。

さらに危険なのが --privileged モードです。これは「ホストの全権限をこのコンテナに与える」のと同義です。コンテナ内のrootユーザーは、ホストのデバイスをマウントしたり、カーネルモジュールをロードしたりできます。この権限があれば、 mount /dev/sda1 /mnt 一発でホストのHDDをマウントし、データを盗み出すのに10分もかかりません。

なぜrootユーザーが最大のセキュリティホールなのか?

核心的な問題は、コンテナ内のroot(UID 0)とホストのroot(UID 0)は、カーネル上では同一人物であるという点です。

「namespaceで隔離されてるんじゃないの?」と思うかもしれません。確かにそうですが、UID namespace はデフォルトでは無効(互換性のため)です。つまり、コンテナ内でrootとして動くプロセスが、何らかの方法で隔離を突破した場合、ホスト上でもroot権限を持っています。

攻撃経路の多く(カーネル脆弱性、設定ミス、権限濫用など)は、root権限に関連しています。非rootユーザーで動かすだけで、これらのリスクの大半を無効化できます。

非rootユーザーの設定:Dockerfileから始める

Docker非rootユーザー設定フロー

Dockerfileでのユーザー作成からランタイムパラメータまで、コンテナリスクを80%下げる手順

⏱️ Estimated time: 30 min

  1. 1

    Step1: Dockerfileで専用ユーザーを作成

    専用ユーザーとグループを作成します。
    • UID/GIDを固定する(例:5000)
    • COPY --chown でファイル所有権を設定
    • インストール完了後に USER 指令を記述
  2. 2

    Step2: ポートバインディング問題を解決

    非rootユーザーは1024番以下のポートを使えません。
    • アプリを3000番などの高ポートで待受させ、リバースプロキシで80番に転送する
    • または NET_BIND_SERVICE Capability を付与する
  3. 3

    Step3: ランタイムセキュリティパラメータの設定

    実行時にセキュリティオプションを追加します。
    • --user で実行ユーザーを指定(上書き)
    • --read-only でファイルシステムを読み取り専用に
    • --security-opt=no-new-privileges で権限昇格を禁止
    • --cap-drop=ALL で不要な権限を剥奪
  4. 4

    Step4: Capabilitiesの最小化

    不要なLinux Capabilitiesを削除します。
    • まず --cap-drop=ALL ですべて消す
    • 必要なもの(NET_BIND_SERVICEなど)だけ --cap-add で追加
  5. 5

    Step5: 強制アクセス制御の有効化

    AppArmor(Debian/Ubuntu)またはSELinux(RHEL/CentOS)を使用します。Dockerのデフォルトプロファイルは強力です。
  6. 6

    Step6: 定期的なスキャンと監視

    Trivyなどでイメージ脆弱性をスキャンし、ランタイムの異常を監視します。

正しい非rootユーザーの作り方

標準的な書き方は以下の通りです:

FROM node:18-alpine

# 専用ユーザーとグループを作成(UID/GIDを指定)
RUN addgroup -g 5000 appgroup \
    && adduser -D -u 5000 -G appgroup appuser

# 作業ディレクトリ設定
WORKDIR /app

# ファイルをコピーして所有権を設定(重要!)
COPY --chown=appuser:appgroup package*.json ./
RUN npm install
COPY --chown=appuser:appgroup . .

# 非rootユーザーに切り替え(これ以降のコマンドはappuser権限で実行)
USER appuser

# アプリ起動
CMD ["node", "server.js"]

ポイント解説:

  1. UID/GIDの指定: addgroup -g 5000 のように番号を固定します。システム任せにすると、データボリュームをマウントした際に権限不一致(Permission Denied)が起きやすくなります。
  2. COPY --chown: 後から RUN chown するとイメージレイヤーが無駄に増え、サイズが大きくなります。コピー時に所有権を設定するのがベストです。
  3. USER命令の位置: npm install などのシステム書き込みが必要な処理はrootで行い、最後に USER で切り替えます。間違えて先頭に書くと、何もインストールできなくなります。

よくある落とし穴と回避策

罠1:ポートバインディング(Permission denied 0.0.0.0:80)
Linuxでは、1024番未満のポートを開くにはroot権限が必要です。

  • 解決策1(推奨): アプリ側でポートを3000や8080に変更し、前段のNginxやロードバランサでポート変換する。
  • 解決策2: NET_BIND_SERVICE Capability を付与する(後述)。

罠2:ログや一時ファイルの書き込み
アプリが /var/log に書き込もうとして失敗するケースです。

  • 解決策: RUN mkdir -p /var/log/myapp && chown appuser /var/log/myapp で専用ディレクトリを作って権限を与える。
  • ベストプラクティス: そもそもファイルに書かず、標準出力(stdout/stderr)に吐き出し、Dockerにログ管理を任せる。

罠3:マウントしたボリュームの権限
ホスト側のディレクトリをマウントしたら、コンテナ内のユーザーが読み書きできないケース。

  • 解決策: ホスト側のディレクトリも chown 5000:5000 で同じUIDに合わせておく。

ランタイムパラメータ:—user とその仲間たち

—user で強制切り替え

Dockerfileに USER が書かれていないイメージでも、実行時に指定できます。

# UID:GID を指定して実行
docker run --user=1001:1001 nginx:latest

# 現在のホストユーザーIDで実行(開発時に便利)
docker run --user="$(id -u):$(id -g)" -v "$PWD:/app" node:18 npm test

特に2番目の方法は、ローカル開発で生成されたファイルがroot所有になって消せなくなる問題を解決できるので超便利です。

読み取り専用ファイルシステム

ハッカーが入ってきても、ファイルシステムが書き込み不可なら何もできません。

docker run -d \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  nginx:alpine

一時ファイルが必要な場所だけ --tmpfs(メモリ上のファイルシステム)をマウントすれば、再起動で綺麗に消えるし、ディスクへの永続化も防げます。

権限昇格の禁止:no-new-privileges

コンテナ内で sudo などを実行しても権限を上げられないようにします。

docker run --security-opt=no-new-privileges myapp

これを付けておけば、万が一コンテナ内にSUIDビットが立ったバイナリがあっても悪用されません。

Capabilities(権限)の最小化

Capabilitiesとは?

Linuxのroot権限を「時計を合わせる」「ネットワーク設定を変える」「ファイルを所有変更する」といった細かい能力(Capabilities)に分割したものです。
Dockerはデフォルトで14個のCapabilitiesを与えていますが、Webアプリには多すぎます。

危険なCapabilities(絶対に渡すな!)

  • SYS_ADMIN: ほぼrootです。マウント操作などが可能になり、脱出の温床になります。
  • NET_ADMIN: ネットワーク設定変更。VPNアプリ以外には不要。
  • SYS_MODULE: カーネルモジュールのロード。論外です。

最小権限で実行する

「全部捨てて、必要なものだけ拾う」のが鉄則です。

docker run -d \
  --cap-drop=ALL \             # 一旦すべて捨てる
  --cap-add=NET_BIND_SERVICE \ # 必要ならポートバインディング権限だけ追加
  --cap-add=CHOWN \            # 必要ならchown権限だけ追加
  myapp

ほとんどのWebアプリは cap-drop=ALL だけで動きます。

まとめ:セキュリティチェックリスト

  1. 脱root: Dockerfileに USER 指令はあるか?
  2. 権限最小化: cap-drop=ALL を試したか?
  3. 読み取り専用: --read-only で動かせるか?
  4. スキャン: イメージの脆弱性スキャン(Trivy等)を定期的に行っているか?

セキュリティは「点」ではなく「継続的なプロセス」です。まずは USER を追加するところから始めてみてください。それだけで、あなたのコンテナはその他大勢よりもずっと安全になります。

FAQ

なぜrootでのコンテナ実行は危険なのですか?
コンテナ内のrootはホストのrootと同じUID 0を持っています。カーネル脆弱性や設定ミスでコンテナから脱出した場合、攻撃者はホストシステム全体の完全な制御権を得てしまうリスクがあるからです。
非rootユーザーで80番ポートをリッスンできません。
Linuxの仕様で、1024番未満の特権ポートはrootが必要です。対策として、アプリのポートを3000番などに変更するか、コンテナ実行時に `--cap-add=NET_BIND_SERVICE` を追加してください。
既存の公式イメージ(root実行)を非root化するには?
Dockerfileを作成し、`FROM` でそのイメージを指定した後、`RUN useradd ...` でユーザーを作成し、`USER` 指令で切り替えてください。または実行時に `docker run --user=UID:GID` を使用して強制的にユーザーを指定することも可能です(ただしファイルのパーミッションに注意が必要です)。
Capabilitiesとは何ですか?
Linuxの特権(root権限)を細分化したものです。例えば `CHOWN`(ファイル所有者変更)、`NET_ADMIN`(ネットワーク設定)などがあります。Dockerはデフォルトで一部を許可していますが、セキュリティ向上のため `--cap-drop=ALL` で不要な権限を剥奪することが推奨されます。

4 min read · 公開日: 2025年12月18日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事