Kubernetes v1.37:bind mount オプションと emptyDir 権限でコンテナストレージを強化

Kubernetes v1.37 で2つのストレージセキュリティ機能が追加:ボリュームマウント上の bind mount オプション(noexec / nosuid / nodev)と emptyDir の作成権限モード(sticky bit 01777 を含む)。前者は 1.24 のセキュリティ監査で指摘されていたギャップを埋めるもので、どちらもまだ Alpha ゲートの背後にある。

日本語
コピー

Kubernetes v1.37:bind mount オプションと emptyDir のパーミッションでコンテナストレージを強化する

Kubernetes v1.37 は重要なストレージセキュリティ機能を導入した。emptyDir のパーミッションモードと bind mount オプションだ。これらはアプリケーション開発者とセキュリティ担当者が厳格なセキュリティポリシーを実装する助けになる。たとえばコンテナ間のファイル削除を禁止したり、書き込み可能なボリュームからの任意のバイナリ実行を禁止したりできる。しかも複雑な回避策なしに、Kubernetes の中で直接行える。

Linux のストレージとパーミッションの基礎

新しい Kubernetes 機能に入る前に、それを可能にしている低レイヤの Linux セキュリティ機構を簡単に振り返っておく。

bind mount フラグ

Linux がディレクトリをマウントまたは再マウントするとき、仮想ファイルシステム(VFS)のフラグがそのファイルシステム上で許可される操作を決める。

  • noexec:このマウントされたファイルシステム上でバイナリを直接実行できない。
  • nosuid:set-user-identifier ビットと set-group-identifier ビットを有効にできない。
  • nodev:このファイルシステム上のキャラクタデバイスやブロックデバイスを解釈しない。

ディレクトリのパーミッションと sticky bit

標準の Unix パーミッションは 3 つの範囲でアクセスを制御する。所有者、グループ、その他(たとえば 07550777)だ。

標準の読み取り・書き込み・実行ビットに加えて、Linux は sticky bit(たとえばモード 01777)をサポートする。ディレクトリに作用する場合、sticky bit はそのディレクトリ内のファイルを削除または改名できるのをファイルの所有者か root だけに保証する。/tmp のような共有の書き込み可能ディレクトリでは、これが必須だ。

この改善が必要とされた理由

Kubernetes に bind mount オプションと emptyDir のパーミッションがなぜ必要なのか。

これらの機能の第一の目的は、セキュリティに関わる bind mount オプションをボリュームマウントで使えるようにすることで、Kubernetes ワークロードのセキュリティを高めることにある。デフォルトでは、ボリュームはコンテナランタイムと kubelet によって bind mount でコンテナにマウントされ、noexecnosuidnodev のフラグが付かない。このデフォルトはセキュリティを弱める。たとえば noexec がなければ、侵害されたプロセスは任意の書き込み可能ボリューム(emptyDir、PersistentVolume など)を利用して任意のバイナリをダウンロードし、chmod +x してから実行できる。コンテナのルートファイルシステムが読み取り専用(readOnlyRootFilesystem: true)であってもだ。noexecnodevnosuid をサポートすることで、ユーザーはセキュリティベンチマークやポリシーに適合するようボリュームマウントを強化するネイティブな手段を得る。

このギャップが最も明白なのは emptyDir ボリュームだ。最も一般的な書き込み可能ボリューム種別であり、複数のセキュリティ指摘の対象でもある。

  • Issue #48912:認識されたセキュリティギャップ——emptyDir にマウントオプションを設定できない点は監査で指摘されていたが、今に至るまで未解決だった。
  • Issue #119627:Kubernetes 1.24 セキュリティ監査(所見番号 NCC-E003660-7HM)——外部監査人は、emptyDirnoexec でマウントできないことがセキュリティ上の失敗に当たると特に指摘した。

だが同じギャップはすべてのボリューム種別に当てはまる。PersistentVolume には mountOptions フィールドがあるが、それらのオプションは CSI ドライバがノード上で適用するファイルシステムレベルのフラグであり、コンテナ内部の bind mount フラグに確実に変換されるわけではない。これまで、コンテナランタイムがあらゆるボリューム種別に対して作成する bind mount に noexecnosuidnodev を設定する仕組みは存在しなかった。

さらに、emptyDir ボリューム種別はディレクトリをデフォルトでハードコードされた 0777 モードで作成する。これはこれまで、このボリュームを見つけられるあらゆるプロセスが、誰が作成したかにかかわらずボリューム内の何でも読み・書き・削除できることを意味していた。

別のアクセスモードを設定するために init コンテナを使うことはできる——今もできる——が、より複雑で、コンプライアンス検証も難しい。

これが現実の問題を生む。

  • emptyDir を共有するマルチコンテナ Pod では、あるコンテナが別のコンテナのファイルを削除するのを防げない。sticky bit(01777)がこの問題を解決するが、それを設定するネイティブな手段はこれまでなかった。
  • 一部のアプリケーションやセキュリティフレームワークは、/tmp ディレクトリに sticky bit(モード 01777)が付いていることを期待する。emptyDir モードを設定するネイティブサポートがなければ、ユーザーは init コンテナや他のボリューム種別でこの要件を満たすしかなかった。
  • より厳格なパーミッション(たとえば所有者とグループだけの 0750)を求めるプラットフォームエンジニアは、chmod を実行する init コンテナを使うしかなく、不要な複雑さが増していた。

emptyDir ボリューム種別は明らかなギャップだ。Kubernetes で最も一般的な書き込み可能ボリューム種別の一つでありながら、作成時のパーミッションを制御する手段が一切なかった。

現実のユースケース

アプリケーション開発者はセキュリティエンジニアと密接に協力し、アプリケーションのセキュリティ態勢を維持し、ワークロードがより広いインフラにリスクをもたらさないようにする。これらの機能により、開発チームは重要なセキュリティシナリオに自信を持って対処できる。

書き込み可能マウントでの権限昇格の防止: 一時的な作業用ボリューム(emptyDir/tmp マウントなど)を構成するアプリケーション開発者は、それらが nosuidnoexec でマウントされることを保証できる。これにより、アプリケーションが侵害されて悪意あるペイロードがダウンロードされたとしても、ワークロードはそのペイロードを実行できず、ましてやそれを使ってノード上の権限を昇格させることもできない。

マルチコンテナ Pod の共有一時領域を保護する: CI/CD パイプラインの Pod を構成する開発者は、ビルドコンテナとログ用 sidecar など複数のコンテナでワークスペースを共有することがよくある。emptyDirmode: 01777 を設定すると、この共有ワークスペースは従来の Unix の /tmp ディレクトリのように振る舞う。各コンテナは独立してファイルを書き込めるが、あるコンテナで侵害されたプロセスが、別のコンテナのビルド成果物を削除することはできない。

アプリケーションデータに最小権限を適用する: データベース Pod をデプロイする開発者は、データベースの一時ストレージへのアクセスを厳密に制限できる。emptyDirmode: 0750 を設定すると、特定のデータベースユーザーとグループだけがそのボリュームを読み書きできるようになり、同じ Pod 内の他のプロセスや sidecar からのアクセスは明確に拒否される。

注意: これらの機能はどちらも Kubernetes v1.37 では Alpha フィーチャーゲートの背後にある。使用するには、API server と kubelet の両方で VolumeBindMountOptionsEmptyDirVolumeMode を有効にする必要がある。

例 1:bind mount オプションを適用する

次の完全な Pod manifest は、emptyDir ボリュームを /tmp にマウントし、bindMountOptions: [noexec, nosuid] を付ける。

apiVersion: v1
kind: Pod
metadata:
  name: hardened-bindmount-pod
  namespace: default
spec:
  os:
    name: linux
  containers:
    - name: hardened-app
      image: alpine:latest
      command: ["sleep", "3600"]
      securityContext:
        readOnlyRootFilesystem: true
      volumeMounts:
        - name: temp-storage
          mountPath: /tmp
          bindMountOptions:
            - noexec
            - nosuid
  volumes:
    - name: temp-storage
      emptyDir: {}

例 2:sticky bit 付きの emptyDir ボリューム権限モード

次の完全な Pod manifest は、mode: 01777emptyDir ボリュームを作成し、コンテナ間で標準的な Unix の /tmp sticky bit 保護を適用する。

apiVersion: v1
kind: Pod
metadata:
  name: hardened-emptydir-pod
  namespace: default
spec:
  os:
    name: linux
  containers:
    - name: app-container
      image: alpine:latest
      command: ["sleep", "3600"]
      volumeMounts:
        - name: shared-tmp
          mountPath: /tmp
  volumes:
    - name: shared-tmp
      emptyDir:
        mode: 01777

Linux でこれらの機能を検証する

これらの機能が実際に制限を強制していることを確認するには、kubectl exec でコンテナに入る。以下の例は、実行しようとしてもこれらの機能によって阻止される操作をシミュレートしたものだ。

noexec を検証する

noexec でマウントされたボリュームにスクリプトを書き込んで実行してみる:

# 1. Exec into the pod
kubectl exec -it hardened-bindmount-pod -- sh

# 2. Create an executable script on the mounted volume
cd /tmp
echo '#!/bin/sh' > test.sh
echo 'echo "Executing untrusted code..."' >> test.sh
chmod +x test.sh

# 3. Attempt to run the script
./test.sh

期待される結果:

sh: ./test.sh: Permission denied

実行ファイルが作成されていても、MS_NOEXEC が bind mount の層で強制されているため、Linux カーネルはその実行を拒否する。

sticky bit を検証する

パーミッションモード 01777emptyDir 内で、別のユーザーのファイルを削除してみる:

# 1. Exec into the pod
kubectl exec -it hardened-emptydir-pod -- sh

# 2. Verify directory permissions on /tmp
ls -ld /tmp
# Output: drwxrwxrwt 2 root root ... /tmp (Notice the 't' indicating sticky bit)

# 3. Create a file as the guest user
su -s /bin/sh -c "touch /tmp/guest_file" guest

# 4. Attempt to delete that file as nobody
su -s /bin/sh -c "rm /tmp/guest_file" nobody

期待される結果:

rm: can't remove '/tmp/guest_file': Operation not permitted

sticky bit(01777)がファイルの削除をファイルの所有者だけに厳密に制限するため、カーネルはこの削除を阻止する。

知っておくべきこと

これらの機能を使い始めるにあたって、覚えておきたい重要な点を以下にまとめる。詳細は bind mount オプション、emptyDir ボリュームモード、emptyDir ボリュームの公式ドキュメントを参照してほしい。

  • デフォルトの動作は変わらない: bindMountOptions を書かない場合、あるいは emptyDirmode を設定しない場合は、これまでとまったく同じ標準のデフォルト動作(たとえば 0777 パーミッション)になる。
  • 対応するボリュームタイプは広い: bindMountOptionsemptyDir、PersistentVolume、CSI ボリューム、projected ボリューム、ConfigMap、Secret などで使用できる。唯一の例外は image ボリュームで、これは明示的に対象外となっている。mode フィールドはすべての emptyDir メディアタイプ(デフォルトのディスク backed、Memory(tmpfs)、HugePages)に適用される。
  • ランタイムの機能が関係する(bindMountOptions の場合): コンテナランタイムは CRI の mount_options フィールドを必ずサポートし、runtimeFeatures でそれを宣言する必要がある。スケジューラは_ノードが宣言した機能_を使って、互換性のないノードに Pod を配置しないようにする。それでも Pod がそのようなノードに到達した場合、kubelet はそれを拒否する。暗黙のフォールバックは存在しない。ただし、emptyDirmode を使う場合、ランタイムのサポートは不要だ。
  • PV の mountOptions とは別物: PersistentVolume の mountOptions は CSI ドライバを通じてストレージ層に作用する。新しい bindMountOptions が制御するのは、ランタイムがコンテナ内部で適用する bind mount フラグだ。両者は異なる層で作用し、互いに競合しない。
  • fsGroup との相互作用: Pod の security context に fsGroup が設定されている場合、fsGroup によって適用されるグループ権限が、emptyDir ボリュームに指定された mode を上書きする。これは Secret および ConfigMap ボリュームにおける defaultMode の既存の動作と一致する。
  • Linux 専用: noexecnosuidnodev といったフラグや Unix のパーミッションモードは Linux の概念だ。bindMountOptions は Windows ノードでは効果がない。Windows では Unix 形式のファイルパーミッションがサポートされていないため、emptyDir ボリュームの mode フィールドもスキップされる。
  • バージョンスキューは安全: これらの機能はどちらも純粋に追加的なものだ。emptyDirmode について:API server でゲートが有効で kubelet で無効の場合、フィールドは受け入れられるが無視され、kubelet は 0777 にフォールバックする。bindMountOptions について:スケジューラは「ノードが宣言した機能」を使って、ランタイムサポートのないノードに Pod を配置しないようにする。Pod がそのようなノードに到達した場合、kubelet はこれらのオプションを黙って無視するのではなく、Pod を拒否する。
  • フィーチャーゲート: これらの機能はどちらも Kubernetes v1.37 で Alpha として提供される:VolumeBindMountOptions:ボリュームマウント上の bind mount フラグを制御する。
  • EmptyDirVolumeModeemptyDir ボリューム作成時のパーミッションモードを制御する。

どうすれば参加できますか?

これらの新機能は SIG Node と SIG Storage が推進しています。詳細は以下の拡張 KEP で確認できます:KEP-5855(bind mount オプション)と KEP-5502(emptyDir パーミッションモード)。

SIG Node への連絡先:

  • Slack: #sig-node
  • メーリングリスト

SIG Storage への連絡先:

  • Slack: #sig-storage
  • メーリングリスト

出典: Kubernetes Blog← ホームへ戻る