「コンテナを使っているから安全」という神話は、現場の最前線では通用しない。
こんにちは。今日もログの荒波を泳いでいるか?
今回は、多くの現場で見逃されがちな、しかし一度突破されればインフラが崩壊しかねない致命的なポイント——「コンテナのroot権限実行」について話をしよう。
「Dockerコンテナの中でrootなのは当たり前じゃないか」と思っているなら、君のサーバーは攻撃者にとって「脱獄(Container Escape)」の準備が整った格好の遊び場だ。今日は、なぜルートレスモードが必要なのか、そして具体的にどう実装すべきかを、実戦的な視点で解説する。
—
1. なぜ「コンテナ内のroot」を放置してはいけないのか
多くのエンジニアが誤解しているが、Dockerコンテナは「仮想マシン(VM)」ほど強固な隔離壁を持っていない。コンテナはあくまでホストOSのカーネルを共有しているプロセスに過ぎないんだ。
攻撃者の視点:コンテナを「踏み台」にする手順
もし君が運用しているWebアプリケーション(PHPやNode.js)にRCE(リモートコード実行)の脆弱性があったとしよう。攻撃者がシェルを奪取した際、まず最初に叩くコマンドはこれだ。
id
# 結果: uid=0(root) gid=0(root) groups=0(root)
コンテナ内で root 権限を持っているということは、カーネルの脆弱性を突いた「コンテナ脱獄」の難易度が劇的に下がることを意味する。
例えば、誤って特権(--privileged)が付与されていたり、ホストの /var/run/docker.sock がマウントされていたりすれば、攻撃者はコンテナの中からホストOS側の全プロセスを掌握し、他のコンテナのデータを盗み、最終的にはクラウド全体の認証情報を奪取するだろう。
これを防ぐための決定打が、「ルートレス(Rootless)実行」だ。
—
2. 実践:Docker/Podmanをルートレスで動かす設計図
「ルートレスモード」とは、Dockerデーモンそのものを一般ユーザー権限で動かす仕組みだ。これにより、万が一コンテナが乗っ取られても、攻撃者が手にできるのは「ホストOS上のただの一般ユーザー権限」に限定される。
Dockerfileを「セキュア」に書き換える
まずは、アプリケーション層での対応だ。多くの公式イメージはデフォルトで root 実行を想定している。これを明示的に拒否する設定を書く。
以下は、Node.js(Express)を例にした、本番環境でそのまま使える堅牢な Dockerfile のサンプルだ。
# --- Build Stage ---
FROM node:18-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install --production
# --- Production Stage ---
FROM node:18-slim
WORKDIR /app
# 1. セキュリティアップデートを適用し、不要なツールは入れない
RUN apt-get update && apt-get upgrade -y && \
rm -rf /var/lib/apt/lists/*
# 2. アプリケーション専用の非特権ユーザーを作成
# nodeユーザーは公式イメージに存在するが、明示的に作成・指定する癖をつけよう
RUN groupadd -r appuser && useradd -r -g appuser -s /sbin/nologin -d /app appuser
# 3. 必要なファイルだけをコピーし、所有権を変更
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --chown=appuser:appuser . .
# 4. root権限を完全に捨てる(ここが最重要)
USER appuser
# 5. コンテナ起動(Node.jsを直接実行し、シェルを介さない)
EXPOSE 3000
CMD ["node", "server.js"]
【盲点】1024番未満のポート制限をどう突破するか?
ルートレスで動かす際、エンジニアが最初につまずくのが「特権ポート(80, 443)」だ。一般ユーザーはこれらのポートをリッスンできない。
解決策:
1. コンテナ内では高位ポート(8080など)を使う。
2. ホスト側のリバースプロキシ(Nginx/WAF)やロードバランサーで変換する。
以下は、Nginxを非特権で動かすための設定例だ。
# nginx.conf (一般ユーザーで実行するための修正)
pid /tmp/nginx.pid; # /var/run/nginx.pid はroot権限が必要なため変更
http {
# ログファイル等も書き込み権限のある場所に逃がす
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
server {
listen 8080; # 80番ではなく8080を使う
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
}
—
3. インフラ層での要塞化:docker-compose.yml の設定
Dockerfileで USER を指定しても、起動オプションで上書きされては意味がない。docker-compose.yml でも制限を二重にかけるのが「多層防御」の鉄則だ。
version: '3.8'
services:
webapp:
build: .
# ホストOSとのユーザーIDマッピングを制限
userns_mode: "host"
# コンテナの書き込み権限を制限(可能な限りReadOnlyにする)
read_only: true
# 一時的な書き込みが必要な場所だけメモリ上にマウント
tmpfs:
- /tmp
- /run
# 特権の昇格を禁止
security_opt:
- no-new-privileges:true
# ケーパビリティ(権限)をすべて捨て、必要なものだけ最小限に足す
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # 1024未満のポートを使いたい場合のみ(基本は不要)
environment:
- NODE_ENV=production
—
4. 現場のインシデントハンドリングから学んだ教訓
私が過去に担当したあるインシデントでは、Webサーバーの脆弱性を突いた攻撃者が、コンテナ内の root 権限を利用して /proc などのシステム情報を詳細にスキャンしていた。
もしそのコンテナがルートレスで動いていれば、攻撃者は /etc/shadow を読むことも、カーネルモジュールに干渉するような高度なエクスプロイトコードを実行することもできなかったはずだ。
導入時の注意点(トラブルシューティング)
- ファイル権限の不整合: ホストからマウントしたボリュームに書き込めない場合がある。これはホスト側のUIDとコンテナ内のUIDが一致していないために起こる。
chownで合わせるか、bind mountではなくnamed volumeを使うのがスマートだ。 - プロセスのゾンビ化: 非特権ユーザーで動かすと、シグナルのハンドリングがうまくいかないことがある。
tiniのような軽量なイニットプロセスをエントリポイントに使うことを検討してくれ。
—
最後に
「動けばいい」という設定は、脆弱性が牙を剥くまでのカウントダウンに過ぎない。
コンテナのルートレス実行は、最初は少し面倒に感じるかもしれない。ポートの設定変更やファイル権限の調整に手間取るだろう。
しかし、その「ひと手間」が、深夜に鳴り響くアラートや、顧客への謝罪行脚を防ぐ最強の盾になるんだ。
今日から、すべての Dockerfile に USER 指定が入っているか確認してほしい。チームの身を守れるのは、君のような細部にこだわるエンジニアだけなのだから。
また次のログで会おう。Stay safe.
コメント