flawopen.com/Teardowns/cve-2024-5321-kubernetes-windows-container-log-permissions
CVE-2024-5321:Kubernetes Windows ノードのコンテナログ権限の不備
CVE-2024-5321(CVSS 6.1、中):Windows ノードの kubelet は Pod のログディレクトリを os.MkdirAll で作成していましたが、この関数は Windows では権限モードを無視するため、ログは BUILTIN\Users に読み取りを、Authenticated Users に変更を許す ACL を継承していました。
病院では患者ごとのカルテを書類キャビネットに保管しています。新しいキャビネットを注文するとき、職員は注文書に「医師のみ」と書きますが、この建物では鍵の欄が黙って無視され、キャビネットは部屋のデフォルトルールをそのまま引き継ぎます。つまり職員なら誰でも読め、入館証があれば誰でも書き込めます。修正策は、新しいキャビネットが届くたびに専用の鍵を取り付け、そのキャビネットについては部屋のルールを外す係を置くことです。
主要な概念と専門用語
DACL / ACE- Windows の任意アクセス制御リスト。どのアカウントがファイルやディレクトリを読み書き・実行できるかを定めるアクセス制御エントリの一覧です。
ACL の継承- 既定では、Windows で新しく作ったファイルやディレクトリは親ディレクトリの DACL のうち継承可能なエントリをコピーします。保護された DACL を設定すると継承されません。
BUILTIN\Users / Authenticated Users- 範囲の広い Windows グループ。すべてのローカルアカウント、および一度でもログオンしたすべてのアカウントです。Kubernetes は ContainerUser で動くコンテナがホストのファイルに触れるとき、BUILTIN\Users として扱います。
Windows 上の os.MkdirAll- Go の
os.MkdirAll(path, perm)がpermを使うのは Unix だけです。Windows ではモードが無視され、ディレクトリは継承された ACL を受け取ります。
根本原因の分析 (Root Cause)
pkg/kubelet/kubelet.go の setupDataDirs() は、Pod ログのルート(C:\var\log\pods)を os.MkdirAll(dir, 0750) で作成していました。Go は Windows でモードを無視するため、このディレクトリと中に書かれるログファイルは親の ACL を継承し、BUILTIN\Users に読み取り、NT AUTHORITY\Authenticated Users に変更の権限が与えられていました。PR #126104 は pkg/util/filesystem に、Unix のモードを明示的な DACL に変換して PROTECTED_DACL_SECURITY_INFORMATION 付きで設定する MkdirAll() と Chmod() を追加し、kubelet のログ・プラグイン・チェックポイント用ディレクトリをそれらに切り替えました。
ステップ・バイ・ステップの攻撃フロー
kubelet がログのルートを作成
Windows ノードで setupDataDirs() が os.MkdirAll( を呼びます。kl.getPodLogsDir(), 0750)0750 は無視され、ディレクトリは親の ACL を継承します。
広いグループが権限を継承
継承されたエントリにより、BUILTIN\Users は Pod のログディレクトリとランタイムがそこに書くファイルを読み取れ、NT AUTHORITY\Authenticated Users は変更できます。
他のワークロードのログを読む
ノード上の低権限ローカルアカウントや、hostPath でログのパスをマウントしたワークロードが、他の Pod の stdout と stderr を読めます。そこにはトークン、接続文字列、顧客データが含まれがちです。
証拠の改ざん
同じアカウントは、ログ転送エージェントが収集する前にログファイルを編集・切り詰め、集中ログから活動を隠せます。
ソースコード比較:脆弱 vs 堅牢化
// pkg/kubelet/kubelet.go (kubelet v1.30.2)
func (kl *Kubelet) setupDataDirs() error {
if err := os.MkdirAll(kl.getRootDir(), 0750); err != nil {
return fmt.Errorf("error creating root directory: %v", err)
}
// BUG on Windows: os.MkdirAll ignores the 0750 mode. The directory inherits
// its parent's ACL, which lets BUILTIN\Users read and
// NT AUTHORITY\Authenticated Users modify every pod's log files.
if err := os.MkdirAll(kl.getPodLogsDir(), 0750); err != nil {
return fmt.Errorf("error creating pod logs root directory %q: %w", kl.getPodLogsDir(), err)
}
if err := os.MkdirAll(kl.getPodsDir(), 0750); err != nil {
return fmt.Errorf("error creating pods directory: %v", err)
}
return nil
}
// pkg/kubelet/kubelet.go (fixed in v1.30.3, PR #126104)
func (kl *Kubelet) setupDataDirs() error {
if err := os.MkdirAll(kl.getRootDir(), 0750); err != nil {
return fmt.Errorf("error creating root directory: %v", err)
}
// FIX: utilfs.MkdirAll is plain os.MkdirAll on Linux. On Windows it also
// calls utilfs.Chmod, which replaces the inherited ACL with an explicit one.
if err := utilfs.MkdirAll(kl.getPodLogsDir(), 0750); err != nil {
return fmt.Errorf("error creating pod logs root directory %q: %w", kl.getPodLogsDir(), err)
}
if err := os.MkdirAll(kl.getPodsDir(), 0750); err != nil {
return fmt.Errorf("error creating pods directory: %v", err)
}
return nil
}
// pkg/util/filesystem/util_windows.go
func MkdirAll(path string, perm os.FileMode) error {
if err := os.MkdirAll(path, perm); err != nil {
return fmt.Errorf("Error creating directory %s: %v", path, err)
}
return Chmod(path, perm)
}
func Chmod(path string, filemode os.FileMode) error {
// Maps owner, group and other mode bits to ACEs for the file's owner SID,
// its group SID and BUILTIN\Users (BU). For 0750 the result is
// "D:(A;OICI;FA;;;<owner>)(A;OICI;FRFX;;;<group>)(A;OICI;;;;BU)".
// (Abridged: the real function builds this string inline.)
dacl, err := daclForMode(path, filemode)
if err != nil {
return err
}
newSD, err := windows.SecurityDescriptorFromString(dacl)
if err != nil {
return fmt.Errorf("Error creating new security descriptor from DACL string: %v", err)
}
newDACL, _, err := newSD.DACL()
if err != nil {
return fmt.Errorf("Error getting DACL from new security descriptor: %v", err)
}
// PROTECTED_DACL_SECURITY_INFORMATION blocks inheritance from the parent,
// so the BUILTIN\Users and Authenticated Users entries no longer apply.
return windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT,
windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
nil, nil, newDACL, nil)
}
エンジニアリング&システム堅牢化チェックリスト
- ✓すべての Windows ノードの kubelet を 1.30.3、1.29.7、1.28.12、1.27.16 以降に更新する。
- ✓
kubectl get nodes -l kubernetes.io/で Windows ノードを洗い出し、各ノードでos=windowsicacls C:\var\log\podsを実行する。BUILTIN\Users や Authenticated Users に読み取り・変更を許すエントリがあってはならない。 - ✓Windows で動く Go コードでは、
os.Mkdir、os.MkdirAll、os.WriteFile、os.Chmodのperm引数に頼らない。windowsで保護された DACL を設定し、Windows の CI ジョブで検証する。.SetNamedSecurityInfo() - ✓信頼できるログエージェント以外のワークロードに、
C:\var\logなどノードのログパスを hostPath でマウントしない。 - ✓コンテナの stdout と stderr に機密情報を出さず、アプリのログではトークンをマスクする。ノードのログディレクトリは Pod 自身より多くの主体が読めるためである。