【実務・中級編】 コンテナのルートレス実行と読み取り専用ファイルシステムの設定 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

おい、ちょっと手を止めてこっちを向いてくれ。

今朝、うちのチームがステージング環境に残していた「あるコンテナ」のログを見たんだがね……背筋が凍る思いをしたよ。開発フィールでは「動けば正義」で済ませがちだが、本番環境でこれをやられたら、数分で会社がニュースのトップを飾る大惨事になるところだった。

多くのエンジニアは「Dockerを使っているから安全だ」「クラウドのマネージドサービスだから大丈夫」と心のどこかで油断している。だが、攻撃者はそんな甘い隙をミリ単位で突いてくる。コンテナのデフォルト設定がいかに危ういか、そしてそれをどうやって「鉄壁の要塞」に変えるのか。今日はその泥臭くて現実的な話をしよう。

—

なぜデフォルトのコンテナは「ザル」なのか?

Dockerをはじめとするコンテナランタイムは、一見するとプロセスが隔離されているように見える。しかし、デフォルトの挙動をそのまま本番に持ち込むのは、玄関の鍵を開けっぱなしにして高級車を路上に停めるようなものだ。

最大の悪夢は、「コンテナ内でのroot実行(UID 0)」と「ルートファイルシステムの書き込み許可」の組み合わせだ。

もし、お前が書いたWebアプリケーションや、依存しているサードパーティのOSSライブラリにリモートコード実行(RCE)の脆弱性が潜んでいたとしよう。攻撃者はその脆弱性を突いてWebコンテナに侵入する。
ここでコンテナがルート権限で動いており、さらにルートファイルシステムが書き込み可能(read-write)だった場合、何が起きるか?

1. 攻撃者はコンテナ内の /app や /usr/bin に悪意のあるバックドアバイナリを書き込む。
2. カーネルの脆弱性(namespaceやcgroupの不備など)を組み合わせて、ホスト側のroot権限へとエスカレーション(特権昇格)を試みる。
3. ホストが乗っ取られ、同じ物理・仮想基盤上で動いている他のすべてのテナントやサービスが芋づる式に陥落する。

「うちはクラウドだから大丈夫」? 甘い。AWSのECSであれ、GKEであれ、コンテナエスケープを許せば、メタデータサービス(169.254.169.254)経由でIAMロールが奪われ、S3やRDSの全データがダークウェブに売りに出されるまで秒読みだ。

—

攻めの視点:コンテナエスケープの現実

百聞は一見に如かずだ。攻撃者がコンテナ内で何を考え、どうやってシステムを破壊しにかかるのか、その手口のリアルを知っておく必要がある。

脆弱なWebアプリを踏み台にした攻撃者は、まず次のようなコマンドでシステムの内部構造を侦察する。

# コンテナ内での侵入確認と権限チェック(攻撃者の視点)
id
# 出力結果が uid=0(root) だったら、攻撃者はガッツポーズをする。

# 書き込み可能なディレクトリを探し、バックドアを仕込む準備をする
mount | grep " / "
# もしここで `rw`(Read-Write)と表示されていたら、ファイルシステムの改ざんが自由自在に行える。

もし、ここに書き込み権限があれば、攻撃者は元のアプリケーションのソースコードを書き換えて永続的なバックドアを仕込んだり、悪意のあるcronジョブを登録したりする。

この悪夢を根絶するための決定打が、「ルートレス実行(Rootless)」と「読み取り専用ファイルシステム(Read-only RootFS)」の強制だ。

—

守りの要:ルートレス × 読み取り専用ファイルシステムの設計

では、具体的にどう設定すべきか。実務の現場でそのまま使える、セキュアなインフラ構成のレシピを公開しよう。

ここでは、最も一般的に使われるDockerおよびKubernetes(K8s)の文脈で、どのように設定を縛るべきかを解説する。

1. Dockerfileレベルでの非特権ユーザー化

まず、コンテナイメージを作る段階で、絶対にrootでプロセスを走らせない強い意志を示そう。Dockerfileの最後の方で、専用のUID/GIDを持つ非特権ユーザーを作成し、USER命令で切り替えるのだ。

# セキュアなDockerfileの実装例
FROM python:3.11-slim

# システムのアップデートと最低限のパッケージインストール
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/api/lists/*

# 専用の非特権ユーザーとグループを作成(システムアカウントとして作成)
RUN groupadd -g 10001 appgroup && \
    useradd -u 10001 -g appgroup -m -s /bin/nologin appuser

# アプリケーションディレクトリの作成と権限設定
WORKDIR /app
COPY --chown=appuser:appgroup ./src /app

# この時点で作業ディレクトリの所有権を非特権ユーザーに渡す
USER 10001:10001

# アプリケーションの起動
EXPOSE 8000
CMD ["python", "main.py"]

ポイントは、USER 10001:10001 と明示的に数字で指定することだ(名前解決の差異によるトラブルを防ぐため)。これにより、万が一コンテナが破られても、そのプロセスはホストどころかコンテナ内ですら権限を持たない「ただの一般ユーザー」に閉じ込められる。

2. Docker Compose / ランタイムでの「読み取り専用ルートFS」の強制

Dockerfileで非特権化しても、デフォルトではルートファイルシステム(/)への書き込みができてしまう。これを完全に封じるのが、read_only: true の設定だ。

しかし、ここで一つ問題が生じる。Webアプリや言語ランタイム(Pythonのキャッシュ、ログ出力、一時ファイルの保存など)は、どうしてもどこかに書き込みを行おうとする。
そのため、「ルートは読み取り専用にしつつ、本当に書き込みが必要な最小限のディレクトリだけを tmpfs(メモリ上の一時領域)としてマウントする」という職人技が必要になる。

以下は、docker-compose.yml における完璧なセキュア設定のサンプルだ。

version: '3.8'

services:
  web-app:
    build: .
    image: my-secure-app:latest
    restart: always
    # 【重要】コンテナのルートファイルシステムを完全に読み取り専用にする
    read_only: true
    # 【重要】rootユーザーでの実行を禁止し、Dockerfileで指定した非特権ユーザーを強制
    user: "10001:10001"
    
    # アプリケーションがどうしても書き込みを必要とする最小限のパスをtmpfsでマウント
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=64m      # 一時ファイル用(実行権限は剥奪)
      - /app/logs:rw,noexec,nosuid,size=128m  # アプリケーションログ出力用
      
    security_opt:
      - no-new-privileges:true              # 特権昇格の新しいプロセス生成を禁止
      
    environment:
      - PYTHONUNBUFFERED=1
    
    ports:
      - "8000:8000"

この設定の肝(キモ)は、tmpfs のオプションに noexec(バイナリの実行禁止)や nosuid(SUIDビットの無効化)を付与している点だ。万が一、攻撃者が /tmp にファイルを書き込めたとしても、そこでスクリプトやマルウェアを実行することは物理的に不可能になる。

—

開発現場の落とし穴:セキュア化に伴う「お作法」の変更

さて、ここまで読んで「よし、明日から全部この設定に変えよう」と思ったそこの君。ちょっと待て。現場にこれを導入すると、間違いなく開発者から悲鳴のクレームが上がる。

よくあるトラブルシューティングと、その対処法をあらかじめ共有しておこう。

1. 「アプリが起動時にログが出せないと言って落ちるんですケド」

  • 原因: アプリケーションが /var/log や /app/logs にログを出そうとしているが、ルートFSが読み取り専用のため拒否されている。
  • 対策: 上記の docker-compose.yml のように、必要なディレクトリを tmpfs として明示的にマウントし、アプリ側でもログ出力先をそのパスに固定すること。

2. 「フレームワークのキャッシュ生成で書き込みエラーが出ます」

  • 原因: Laravel, Django, Next.jsなどのフレームワークは、デフォルトでソースコードと同じディレクトリにキャッシュやコンパイル済みファイルを書き込もうとする。
  • 対策: 環境変数や設定ファイルを調整し、キャッシュの出力先を /tmp 配下に強制変更すること。例えばPythonのbytecode生成を抑制するには、環境変数に PYTHONDONTWRITEBYTECODE=1 を設定すればいい。

—

シーフエンジニアからのメッセージ

セキュリティ対策というのは、往々にして「開発の利便性」と「安全性」のトレードオフになりがちだ。だが、今回紹介した「ルートレス実行」と「読み取り専用ファイルシステム」は、一度設計に組み込んでしまえば、日々の運用コストを一切増やすことなく、攻撃者に対する「圧倒的な鉄壁の防御壁」として機能し続ける。

「動けばいいや」のコードは、いつか会社を揺るがすブーメランとなって自分に戻ってくる。
今日から君のプロジェクトでも、コンテナのデフォルト設定を見直し、無駄な権限を剥ぎ取って本当の意味での「要塞化」を完了させてくれ。期待しているぞ。

コメント

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