こんにちは。セキュリティチーフエンジニアのSysGuardです。
日々の開発やインフラ構築、本当にお疲れ様です。モダンなWebアプリケーション開発において、DockerやKubernetesを用いたコンテナオーケストレーションはもはや「標準インフラ」と言えます。しかし、現場で多くのインシデントハンドリングやペネトレーションテスト(擬似攻撃演習)を行ってきた身として、未だに多くの環境で見過ごされている致命的な盲点があります。
それが、「コンテナのルートファイルシステムが書き込み可能なまま放置されている」という事実です。
「コンテナは使い捨てだから、汚されても再起動すればいい」と考えていませんか? その油断こそが、攻撃者にとって絶好の足がかりになります。今回は、攻撃者がコンテナに侵入した後にどのような挙動を見せるのか、そしてそれを根本から封殺する最強の防御策である「ルートファイルシステムの読み取り専用化(readOnlyRootFilesystem)」の実装方法と、導入時に必ず直面する「アプリが動かなくなる問題」のスマートな解決策を解説します。
—
1. ペネトレーションテストの現場から:なぜ攻撃者は「書き込み権限」を欲するのか
まずは、攻撃者の視点(レッドチームの目線)に立って、コンテナ環境がどのように攻略されるかを理解しましょう。
Webアプリケーションにリモートコード実行(RCE)やファイルアップロードの脆弱性が存在した場合、攻撃者はコンテナ内部で任意のコマンドを実行できるようになります。この段階では、まだ「コンテナ内の低権限ユーザー」に過ぎないことも多いのですが、攻撃者が次に行うステップは決まっています。「環境の偵察」と「永続化・権限昇格のためのツール配置」です。
攻撃者の一般的なエクスプロイトフロー
1. 侵入: Webアプリの脆弱性を突き、リバースシェルを確立。
2. ツールのダウンロード: ターゲット環境に curl や wget を用いて、より高度なスキャンツール(PEASSなど)や、C2(コマンド&コントロール)サーバーと通信するためのマルウェアを送り込む。
3. 設置場所: 通常、書き込みが許可されている /tmp や、あるいは /var/tmp、/dev/shm などのディレクトリに実行ファイルを設置し、chmod +x で実行権限を与えて起動する。
もし、コンテナのルートファイルシステムがデフォルトの「書き込み可能(Read-Write)」のままであれば、攻撃者はコンテナ内のあらゆる場所に自由にファイルを配置できます。ライブラリを改ざんしてバックドアを仕込むことも、不正なCronジョブを登録することも容易です。
逆に言えば、「ファイルシステムへの新規書き込みを一切禁止する」だけで、攻撃者の手足の大部分を縛り上げることができるのです。
—
2. 対策の本命:readOnlyRootFilesystem の設定方法
コンテナを不変(Immutable)な存在にするため、KubernetesやDockerにはルートファイルシステムを読み取り専用にするオプションが用意されています。
2-1. Kubernetesにおける設定例
Kubernetesでは、Podの securityContext 内で readOnlyRootFilesystem: true を明示的に指定します。
以下に、セキュアに構成されたマニフェストの構成例を示します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-web-app
labels:
app: secure-web
spec:
replicas: 2
selector:
matchLabels:
app: secure-web
template:
metadata:
labels:
app: secure-web
spec:
containers:
- name: web-container
image: nginx:alpine
# ----------------------------------------------------
# セキュリティコンテキストの設定
# ----------------------------------------------------
securityContext:
# ルートファイルシステムを読み取り専用に強制
readOnlyRootFilesystem: true
# 特権昇格の禁止
allowPrivilegeEscalation: false
# 非ルートユーザーでの実行を推奨
runAsNonRoot: true
runAsUser: 10001
ports:
- containerPort: 8080
2-2. Docker単体(Docker Compose)での設定例
Docker Composeを使用している場合も、read_only: true を指定することで同様の制限を課すことができます。
version: '3.8'
services:
web:
image: my-app-image:latest
ports:
- "8080:8080"
# ルートファイルシステムを読み取り専用に設定
read_only: true
# 必要最小限の権限のみを付与(特権の排除)
cap_drop:
- ALL
user: "1000:1000"
—
3. 現場のジレンマ:読み取り専用にするとアプリが動かない問題
「よし、これでセキュリティは万全だ!」と上記の設定を本番環境にデプロイすると、高確率でアプリケーションが起動エラーを起こすか、特定の機能(ファイルのアップロード、セッション保存、キャッシュ生成など)が動作しなくなります。
なぜなら、多くのWebフレームワークやミドルウェア(NginxやApacheなど)は、内部的に /tmp への一時ファイル書き込みや、/var/run へのPIDファイルの作成、/var/log へのログ出力を行うように設計されているからです。
これを解決するために、「基本は読み取り専用にしつつ、書き込みが必要な特定のディレクトリだけをメモリ上の一時領域(emptyDir)にマウントして逃がす」という設計テクニックを使用します。
実務で使える!Kubernetes完全セキュアマニフェスト(Nginx+PHP-FPM想定)
以下のコード例は、ルートファイルシステムを完全にロックダウンしつつ、Nginxやアプリが必要とする一時ディレクトリだけを書き込み可能にする、コピペで使える実践的な設定テンプレートです。
apiVersion: apps/v1
kind: Deployment
metadata:
name: hardened-nginx
spec:
replicas: 2
selector:
matchLabels:
app: hardened-nginx
template:
metadata:
labels:
app: hardened-nginx
spec:
containers:
- name: nginx
image: nginx:alpine
securityContext:
# ルートファイルシステムを読み取り専用に
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 101 # nginx公式非特権ユーザー
ports:
- containerPort: 8080
# 書き込みが必要なパスに、一時ボリュームをマウント
volumeMounts:
- name: cache-volume
mountPath: /var/cache/nginx
- name: run-volume
mountPath: /var/run
- name: tmp-volume
mountPath: /tmp
# ボリュームの定義
volumes:
# emptyDirをmedium: Memoryに設定することで、
# ホストのディスクではなくRAM上に一時領域を確保し、高速かつ安全に処理します。
- name: cache-volume
emptyDir:
medium: Memory
- name: run-volume
emptyDir:
medium: Memory
- name: tmp-volume
emptyDir:
medium: Memory
この設定の解説
emptyDirボリュームは、Podが起動している間だけ存在する一時的なストレージです。medium: Memoryを指定することで、ファイルシステムではなくメモリ(RAM)上に領域が確保されます。これにより、I/Oが高速化されるだけでなく、コンテナが再起動または削除された瞬間に、一時ファイルやキャッシュデータが物理ディスクに残ることなく完全に消去されるため、フォレンジック対策やデータ漏洩防止の観点からも極めて強力です。
—
4. 開発時の注意点:Dockerfileビルド時のベストプラクティス
コンテナランタイム(Kubernetes等)側での制御に加え、コンテナイメージをビルドする段階(Dockerfile)から、読み取り専用環境を意識した設計を行う必要があります。
以下のDockerfileは、Python (Flask) アプリケーションを例に、安全なコンテナ構築のプラクティスを示したものです。
# マルチステージビルドを使用して、不要なビルドツールを最終イメージに残さない
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# 実行用軽量イメージ
FROM python:3.11-slim AS runner
WORKDIR /app
# ビルダーから依存ライブラリのみをコピー
COPY --from=builder /root/.local /sbin/nonroot/.local
COPY . .
# 非特権システムユーザーの作成 (UID/GID 10001)
RUN groupadd -g 10001 appgroup && \
useradd -u 10001 -g appgroup -s /bin/false appuser
# アプリケーションディレクトリの所有者を変更
RUN chown -R appuser:appgroup /app
USER 10001
# 環境変数の設定
ENV PATH=/sbin/nonroot/.local/bin:$PATH
ENV PORT=8080
# アプリケーションが一時的に使用するキャッシュディレクトリなどを明示
# ※Kubernetesマニフェスト側で、ここをemptyDirとしてマウントする
VOLUME ["/tmp"]
EXPOSE 8080
CMD ["gunicorn", "-b", "0.0.0.0:8080", "app:app"]
実務でのTips
1. ログの標準出力(Stdout)化: コンテナ内にログファイルを書き出す設計は避けましょう。アプリケーションのログはすべて標準出力(stdout)および標準エラー出力(stderr)に吐き出し、FluentbitやPromtailなどのコレクター経由で外部のログ監視システム(CloudWatch, Datadog, Grafana Loki等)に転送するのがクラウドネイティブな鉄則です。これにより、/var/log への書き込み権限すら不要になります。
2. 静的アセットの外部化: ユーザーがアップロードする画像ファイルなどは、コンテナのローカルディスクに保存するのではなく、Amazon S3などのオブジェクトストレージに直接、あるいは署名付きURLを用いてアップロードする設計にシフトしてください。
—
5. まとめ:不便さの先にある「絶対的な安心感」
コンテナの readOnlyRootFilesystem 設定は、導入初期において「パーミッションエラー」との戦いになるため、開発メンバーから敬遠されがちな技術です。しかし、この設定がもたらす防御効果は計り知れません。
仮にWebアプリケーションに「任意のファイルを書き込める脆弱性」が見つかったとしても、コンテナのルートファイルシステムが書き込み禁止であれば、攻撃者はWebシェル(バックドア)を設置することも、マルウェアをダウンロードして実行することもできません。攻撃の連鎖(エクスプロイトチェーン)を、最初の「侵入・設置」の段階で完全に遮断できるのです。
「セキュアな設計は、最初の設計段階でのみ低コストで導入できる」
ぜひ、次回のスプリントや新規プロジェクトの立ち上げ時に、この設定をデフォルトの共通テンプレートとして組み込んでみてください。
何か設定や設計で迷うことがあれば、いつでもチームのミーティングで相談してくださいね。堅牢なシステムを一緒に作っていきましょう!
コメント