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%は低減できるはずです。
なぜ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
Step1: Dockerfileで専用ユーザーを作成
専用ユーザーとグループを作成します。
• UID/GIDを固定する(例:5000)
• COPY --chown でファイル所有権を設定
• インストール完了後に USER 指令を記述 - 2
Step2: ポートバインディング問題を解決
非rootユーザーは1024番以下のポートを使えません。
• アプリを3000番などの高ポートで待受させ、リバースプロキシで80番に転送する
• または NET_BIND_SERVICE Capability を付与する - 3
Step3: ランタイムセキュリティパラメータの設定
実行時にセキュリティオプションを追加します。
• --user で実行ユーザーを指定(上書き)
• --read-only でファイルシステムを読み取り専用に
• --security-opt=no-new-privileges で権限昇格を禁止
• --cap-drop=ALL で不要な権限を剥奪 - 4
Step4: Capabilitiesの最小化
不要なLinux Capabilitiesを削除します。
• まず --cap-drop=ALL ですべて消す
• 必要なもの(NET_BIND_SERVICEなど)だけ --cap-add で追加 - 5
Step5: 強制アクセス制御の有効化
AppArmor(Debian/Ubuntu)またはSELinux(RHEL/CentOS)を使用します。Dockerのデフォルトプロファイルは強力です。 - 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"]ポイント解説:
- UID/GIDの指定:
addgroup -g 5000のように番号を固定します。システム任せにすると、データボリュームをマウントした際に権限不一致(Permission Denied)が起きやすくなります。 COPY --chown: 後からRUN chownするとイメージレイヤーが無駄に増え、サイズが大きくなります。コピー時に所有権を設定するのがベストです。- USER命令の位置:
npm installなどのシステム書き込みが必要な処理はrootで行い、最後にUSERで切り替えます。間違えて先頭に書くと、何もインストールできなくなります。
よくある落とし穴と回避策
罠1:ポートバインディング(Permission denied 0.0.0.0:80)
Linuxでは、1024番未満のポートを開くにはroot権限が必要です。
- 解決策1(推奨): アプリ側でポートを3000や8080に変更し、前段のNginxやロードバランサでポート変換する。
- 解決策2:
NET_BIND_SERVICECapability を付与する(後述)。
罠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 だけで動きます。
まとめ:セキュリティチェックリスト
- 脱root: Dockerfileに
USER指令はあるか? - 権限最小化:
cap-drop=ALLを試したか? - 読み取り専用:
--read-onlyで動かせるか? - スキャン: イメージの脆弱性スキャン(Trivy等)を定期的に行っているか?
セキュリティは「点」ではなく「継続的なプロセス」です。まずは USER を追加するところから始めてみてください。それだけで、あなたのコンテナはその他大勢よりもずっと安全になります。
FAQ
なぜrootでのコンテナ実行は危険なのですか?
非rootユーザーで80番ポートをリッスンできません。
既存の公式イメージ(root実行)を非root化するには?
Capabilitiesとは何ですか?
4 min read · 公開日: 2025年12月18日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


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