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 つの範囲でアクセスを制御する。所有者、グループ、その他(たとえば 0755 や 0777)だ。
標準の読み取り・書き込み・実行ビットに加えて、Linux は sticky bit(たとえばモード 01777)をサポートする。ディレクトリに作用する場合、sticky bit はそのディレクトリ内のファイルを削除または改名できるのをファイルの所有者か root だけに保証する。/tmp のような共有の書き込み可能ディレクトリでは、これが必須だ。
この改善が必要とされた理由
Kubernetes に bind mount オプションと emptyDir のパーミッションがなぜ必要なのか。
これらの機能の第一の目的は、セキュリティに関わる bind mount オプションをボリュームマウントで使えるようにすることで、Kubernetes ワークロードのセキュリティを高めることにある。デフォルトでは、ボリュームはコンテナランタイムと kubelet によって bind mount でコンテナにマウントされ、noexec、nosuid、nodev のフラグが付かない。このデフォルトはセキュリティを弱める。たとえば noexec がなければ、侵害されたプロセスは任意の書き込み可能ボリューム(emptyDir、PersistentVolume など)を利用して任意のバイナリをダウンロードし、chmod +x してから実行できる。コンテナのルートファイルシステムが読み取り専用(readOnlyRootFilesystem: true)であってもだ。noexec、nodev、nosuid をサポートすることで、ユーザーはセキュリティベンチマークやポリシーに適合するようボリュームマウントを強化するネイティブな手段を得る。
このギャップが最も明白なのは emptyDir ボリュームだ。最も一般的な書き込み可能ボリューム種別であり、複数のセキュリティ指摘の対象でもある。
- Issue #48912:認識されたセキュリティギャップ——
emptyDirにマウントオプションを設定できない点は監査で指摘されていたが、今に至るまで未解決だった。 - Issue #119627:Kubernetes 1.24 セキュリティ監査(所見番号 NCC-E003660-7HM)——外部監査人は、
emptyDirをnoexecでマウントできないことがセキュリティ上の失敗に当たると特に指摘した。
だが同じギャップはすべてのボリューム種別に当てはまる。PersistentVolume には mountOptions フィールドがあるが、それらのオプションは CSI ドライバがノード上で適用するファイルシステムレベルのフラグであり、コンテナ内部の bind mount フラグに確実に変換されるわけではない。これまで、コンテナランタイムがあらゆるボリューム種別に対して作成する bind mount に noexec、nosuid、nodev を設定する仕組みは存在しなかった。
さらに、emptyDir ボリューム種別はディレクトリをデフォルトでハードコードされた 0777 モードで作成する。これはこれまで、このボリュームを見つけられるあらゆるプロセスが、誰が作成したかにかかわらずボリューム内の何でも読み・書き・削除できることを意味していた。
別のアクセスモードを設定するために init コンテナを使うことはできる——今もできる——が、より複雑で、コンプライアンス検証も難しい。
これが現実の問題を生む。
emptyDirを共有するマルチコンテナ Pod では、あるコンテナが別のコンテナのファイルを削除するのを防げない。sticky bit(01777)がこの問題を解決するが、それを設定するネイティブな手段はこれまでなかった。- 一部のアプリケーションやセキュリティフレームワークは、
/tmpディレクトリに sticky bit(モード01777)が付いていることを期待する。emptyDirモードを設定するネイティブサポートがなければ、ユーザーは init コンテナや他のボリューム種別でこの要件を満たすしかなかった。 - より厳格なパーミッション(たとえば所有者とグループだけの
0750)を求めるプラットフォームエンジニアは、chmodを実行する init コンテナを使うしかなく、不要な複雑さが増していた。
emptyDir ボリューム種別は明らかなギャップだ。Kubernetes で最も一般的な書き込み可能ボリューム種別の一つでありながら、作成時のパーミッションを制御する手段が一切なかった。
現実のユースケース
アプリケーション開発者はセキュリティエンジニアと密接に協力し、アプリケーションのセキュリティ態勢を維持し、ワークロードがより広いインフラにリスクをもたらさないようにする。これらの機能により、開発チームは重要なセキュリティシナリオに自信を持って対処できる。
書き込み可能マウントでの権限昇格の防止: 一時的な作業用ボリューム(emptyDir や /tmp マウントなど)を構成するアプリケーション開発者は、それらが nosuid と noexec でマウントされることを保証できる。これにより、アプリケーションが侵害されて悪意あるペイロードがダウンロードされたとしても、ワークロードはそのペイロードを実行できず、ましてやそれを使ってノード上の権限を昇格させることもできない。
マルチコンテナ Pod の共有一時領域を保護する: CI/CD パイプラインの Pod を構成する開発者は、ビルドコンテナとログ用 sidecar など複数のコンテナでワークスペースを共有することがよくある。emptyDir に mode: 01777 を設定すると、この共有ワークスペースは従来の Unix の /tmp ディレクトリのように振る舞う。各コンテナは独立してファイルを書き込めるが、あるコンテナで侵害されたプロセスが、別のコンテナのビルド成果物を削除することはできない。
アプリケーションデータに最小権限を適用する: データベース Pod をデプロイする開発者は、データベースの一時ストレージへのアクセスを厳密に制限できる。emptyDir に mode: 0750 を設定すると、特定のデータベースユーザーとグループだけがそのボリュームを読み書きできるようになり、同じ Pod 内の他のプロセスや sidecar からのアクセスは明確に拒否される。
注意: これらの機能はどちらも Kubernetes v1.37 では Alpha フィーチャーゲートの背後にある。使用するには、API server と kubelet の両方で VolumeBindMountOptions と EmptyDirVolumeMode を有効にする必要がある。
例 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: 01777 で emptyDir ボリュームを作成し、コンテナ間で標準的な 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 を検証する
パーミッションモード 01777 の emptyDir 内で、別のユーザーのファイルを削除してみる:
# 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を書かない場合、あるいはemptyDirにmodeを設定しない場合は、これまでとまったく同じ標準のデフォルト動作(たとえば0777パーミッション)になる。 - 対応するボリュームタイプは広い:
bindMountOptionsはemptyDir、PersistentVolume、CSI ボリューム、projected ボリューム、ConfigMap、Secret などで使用できる。唯一の例外は image ボリュームで、これは明示的に対象外となっている。modeフィールドはすべてのemptyDirメディアタイプ(デフォルトのディスク backed、Memory(tmpfs)、HugePages)に適用される。 - ランタイムの機能が関係する(
bindMountOptionsの場合): コンテナランタイムは CRI のmount_optionsフィールドを必ずサポートし、runtimeFeaturesでそれを宣言する必要がある。スケジューラは_ノードが宣言した機能_を使って、互換性のないノードに Pod を配置しないようにする。それでも Pod がそのようなノードに到達した場合、kubelet はそれを拒否する。暗黙のフォールバックは存在しない。ただし、emptyDirにmodeを使う場合、ランタイムのサポートは不要だ。 - PV の mountOptions とは別物: PersistentVolume の
mountOptionsは CSI ドライバを通じてストレージ層に作用する。新しいbindMountOptionsが制御するのは、ランタイムがコンテナ内部で適用する bind mount フラグだ。両者は異なる層で作用し、互いに競合しない。 - fsGroup との相互作用: Pod の security context に
fsGroupが設定されている場合、fsGroupによって適用されるグループ権限が、emptyDirボリュームに指定されたmodeを上書きする。これは Secret および ConfigMap ボリュームにおけるdefaultModeの既存の動作と一致する。 - Linux 専用:
noexec、nosuid、nodevといったフラグや Unix のパーミッションモードは Linux の概念だ。bindMountOptionsは Windows ノードでは効果がない。Windows では Unix 形式のファイルパーミッションがサポートされていないため、emptyDirボリュームのmodeフィールドもスキップされる。 - バージョンスキューは安全: これらの機能はどちらも純粋に追加的なものだ。
emptyDirのmodeについて:API server でゲートが有効で kubelet で無効の場合、フィールドは受け入れられるが無視され、kubelet は0777にフォールバックする。bindMountOptionsについて:スケジューラは「ノードが宣言した機能」を使って、ランタイムサポートのないノードに Pod を配置しないようにする。Pod がそのようなノードに到達した場合、kubelet はこれらのオプションを黙って無視するのではなく、Pod を拒否する。 - フィーチャーゲート: これらの機能はどちらも Kubernetes v1.37 で Alpha として提供される:
VolumeBindMountOptions:ボリュームマウント上の bind mount フラグを制御する。 EmptyDirVolumeMode:emptyDirボリューム作成時のパーミッションモードを制御する。
どうすれば参加できますか?
これらの新機能は SIG Node と SIG Storage が推進しています。詳細は以下の拡張 KEP で確認できます:KEP-5855(bind mount オプション)と KEP-5502(emptyDir パーミッションモード)。
SIG Node への連絡先:
- Slack: #sig-node
- メーリングリスト
SIG Storage への連絡先:
- Slack: #sig-storage
- メーリングリスト