【実務・中級編】 コンテナの読み取り専用ファイルシステム(Read-only Root Filesystem) – インフラ・ネットワーク & クラウドセキュリティ防御ガイド

おい、ちょっと手を止めてくれ。先日のペネトレーションテストのログを見直していたんだが、相変わらずWebアプリケーションの脆弱性を踏み台にしたコンテナ内への侵入・改ざんの痕跡が後を絶たない。

「うちのコンテナは最小限のベースイメージを使っているから大丈夫だ」
「コンテナはイミュータブル(使い捨て)だから永続化なんてされない」

……そんな甘い考えを持っているなら、今日でその頭を完全にリセットしてほしい。現場のインシデントハンドリングをナメてもらっては困る。攻撃者は、我々が想像もしないような経路で侵入し、書き込み可能な領域(主にルートファイルシステム)にWebシェルを仕込み、そこから水平展開(ピボット)を狙っている。

今日は、そんな泥臭い攻撃手法を物理的に無力化するための切り札、「コンテナの読み取り専用ファイルシステム(Read-only Root Filesystem)」について徹底的に叩き込んでやる。教科書には載っていない、現場のリアルな防衛術を一緒に見ていこう。

—

1. なぜ「読み取り専用(Read-only)」が最強の防衛策になるのか?

コンテナのデフォルト設定では、ルートファイルシステム(/)は読み書き可能(Read-Write)になっている。これが何を意味するか分かるか?

例えば、動かしているWebアプリケーションに任意のファイルアップロード脆弱性や、リモートコード実行(RCE)の脆弱性が存在したとする。攻撃者はそこを突き、コンテナ内の /var/www/html/ や、最悪の場合はシステムバイナリが存在するディレクトリに直接悪意あるスクリプトやマルウェアを書き込む。

# 攻撃者がRCE経由で実行する典型的なコマンドのイメージ
echo "<?php system(\$_GET['cmd']); ?>" > /var/www/html/backdoor.php

もし、ルートファイルシステムを Read-only にしておけば、攻撃者がどれだけ見事なRCEをキメようとも、上記のような書き込みコマンドは容赦なく Read-only file system エラーで弾き返される。
アプリのプロセスが動作する上で必要な書き込み先(ログやセッション、一時ファイルなど)だけを明示的に tmpfs(メモリ上のファイルシステム)やボリュームとして切り出し、それ以外は一切の改ざんを許さない。これが、攻撃者のキルチェーンを初期段階で完全に破壊する「要塞化」の基本だ。

—

2. 現場で直面する罠:Read-only化するとアプリが即死する理由

「よし、じゃあ今日のデプロイから全部のコンテナを Read-only にしてやろう!」――待て、慌てるな。それを安易に本番環境でやると、秒でアプリケーションがクラッシュして障害アラートが鳴り響くことになる。

なぜなら、世の中の多くのフレームワークやミドルウェアは、「動的なファイル書き込みがルート直下やアプリディレクトリで行われること」を前提に作られているからだ。
例えば、以下のような処理がコンテナ内で動いているだけで、Read-only環境では即座に例外を吐いて落ちる。

  • テンプレートエンジン(TwigやSmartyなど)がキャッシュファイルをコンパイルしてローカルに保存しようとする。
  • フレームワークがセッションファイルや一時ログをデフォルトのパスに書き込もうとする。
  • 画像アップロード機能を実装したPHPアプリが、ストレージをマウントせずにローカルの uploads/ ディレクトリに書き込もうとする。

これらをすべてクリアするために、インフラエンジニアと開発者がスクラムを組んで「書き込みが必要なパス」を正確に特定し、適切にボリュームや tmpfs として分離する設計を行わなければならない。

—

3. 実装ハンズオン:Docker Compose & Kubernetes での構成例

では、具体的にどう設定するのか。百聞は一見に如かずだ。ここでは実務で最もよく使われる Docker Compose と Kubernetes (K8s) の設定サンプルを示す。コピペして検証環境で試してほしい。

3.1. Docker Composeでルートを厳格にロックする設定

docker-compose.yml において、コンテナのルートを読み取り専用にしつつ、アプリケーションがどうしても書き込む必要のあるディレクトリだけを tmpfs(メモリ上)やバインドマウントで逃がす黄金パターンの設定だ。

version: '3.8'

services:
  web_app:
    image: my-php-app:latest
    container_name: hardened_web_app
    # ルートファイルシステムを完全に読み取り専用にする
    read_only: true
    
    # アプリケーションがどうしても書き込みを必要とする最小限のパスをtmpfs(揮発性メモリ)でマウント
    tmpfs:
      - /run          # PIDファイルやプロセス管理用
      - /tmp          # 一時ファイル用
      - /var/cache    # フレームワークのキャッシュ用
      - /var/log/nginx # Nginxのログ用(必要に応じて外部転送が望ましいがローカル出力する場合)

    # 永続化が必要なアップロード画像などは、専用のボリューム(または外付けストレージ)を明示的にマウント
    volumes:
      - app_uploads:/var/www/html/public/uploads:rw

    ports:
      - "80:80"
    security_opt:
      - no-new-privileges:true # 権限昇格を防ぐ鉄則設定

volumes:
  app_uploads:
    driver: local

この設定により、/var/www/html の本体は読み取り専用で保護されつつ、アップロード機能だけが安全に動作する環境が構築できる。

3.2. Kubernetes (K8s) での Pod Security Standards 準拠設定

Kubernetes環境であれば、securityContext を用いてPod単位、あるいはコンテナ単位でファイルシステムの読み取り専用を強制する。

apiVersion: v1
kind: Pod
metadata:
  name: secure-webapp-pod
  namespace: production
spec:
  containers:
  - name: webapp
    image: my-python-app:latest
    securityContext:
      # ルートファイルシステムの書き込みを禁止
      readOnlyRootFilesystem: true
      # 特権昇格の防止(必須)
      allowPrivilegeEscalation: false
      runAsNonRoot: true
      runAsUser: 1000
    volumeMounts:
      # 一時的に書き込みが必要なディレクトリにはemptyDir(tmpfs等)を割り当てる
      - mountPath: /app/tmp
        name: tmp-dir
      - mountPath: /app/logs
        name: log-dir
  volumes:
  - name: tmp-dir
    emptyDir: {}
  - name: log-dir
    emptyDir: {}

—

4. 開発者必見:セキュアなコード設計のプラクティス

インフラ側で Read-only にしても、開発側がそれを無視した実装をしているとアプリが破綻する。チームの開発者には以下のルールを徹底させてほしい。

良い例:一時ファイルの作成にシステム標準の /tmp を利用する

PHPやPythonなどで一時ファイルを扱う際、アプリケーションのソースコードディレクトリ直下にファイルを生成するのではなく、必ずOSが提供する安全な一時ディレクトリ(tmpfs でマウントされた領域)を利用する。

<?php
// 【良い例】読み取り専用環境でも確実に書き込める /tmp を利用する
$temp_file = tempnam('/tmp', 'sec_upload_');

if ($temp_file === false) {
    throw new Exception('一時ファイルの生成に失敗しました。');
}

// ファイルへの書き込み処理
file_put_contents($temp_file, $uploaded_data);

// 処理が終わったら確実に削除
// unlink($temp_file);
?>
# 【Pythonの良い例】tempfileモジュールを使い、/tmp 配下に安全に一時ファイルを生成する
import tempfile
import os

def process_secure_upload(file_data):
    # read-only環境でも安全な /tmp 配下に一時ファイルを作成
    with tempfile.NamedTemporaryFile(dir='/tmp', delete=False, prefix='sec_') as temp_file:
        temp_file.write(file_data)
        temp_file_path = temp_file.name
    
    print(f"一時ファイルが安全に作成されました: {temp_file_path}")
    # 以降の処理...

反対に、dirname(__FILE__) . '/uploads/temp.txt' のように、ソースコードと同じ階層に一時ファイルを書き込もうとするレガシーな実装は、Read-only環境下では Permission denied または Read-only file system エラーを引き起こすため、直ちにリファクタリング対象としなければならない。

—

5. まとめ:セキュリティは「諦め」をいかに強制するかである

セキュリティの極意は、攻撃者に「ここは攻め込んでも旨味がない、コストが高すぎる」と思わせることにある。
コンテナのルートファイルシステムを読み取り専用(Read-only)にすることは、万が一Webアプリケーションに致命的な脆弱性が発覚し、RCEを許したとしても、「侵入者はその場から一歩も動けず、ファイルを一切改ざんできない」という圧倒的なアドバンテージを我々にもたらしてくれる。

「動かないからとりあえずRead-Writeに戻す」という安易な妥協は、セキュリティホールを自らドブに捨てるようなものだ。
インフラと開発が一体となり、必要な書き込み先を正しく切り分け、イミュータブルで堅牢なコンテナインフラを築き上げてほしい。君たちの手腕にかかっている。健闘を祈る。

コメント

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