flawopen.com/Teardowns/cve-2024-5321-kubernetes-windows-container-log-permissions
CVE-2024-5321: Kubernetes Windows 노드의 컨테이너 로그 권한 문제
CVE-2024-5321(CVSS 6.1, 중간): Windows 노드의 kubelet은 파드 로그 디렉터리를 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)은 Unix에서만perm을 사용합니다. Windows에서는 모드가 무시되고 디렉터리는 상속된 ACL을 받습니다.
근본 원인 분석 (Root Cause)
pkg/kubelet/kubelet.go의 setupDataDirs()는 파드 로그 루트(C:\var\log\pods)를 os.MkdirAll(dir, 0750)으로 만들었습니다. Go는 Windows에서 모드를 무시하므로 이 디렉터리와 그 안에 쓰이는 로그 파일은 부모의 ACL을 상속했고, BUILTIN\Users는 읽기, NT AUTHORITY\Authenticated Users는 수정 권한을 가졌습니다. PR #126104는 Unix 모드를 명시적 DACL로 바꿔 PROTECTED_DACL_SECURITY_INFORMATION으로 적용하는 MkdirAll()과 Chmod()를 pkg/util/filesystem에 추가하고, kubelet의 로그·플러그인·체크포인트 디렉터리가 이를 쓰도록 바꿨습니다.
단계별 공격 실행 흐름
kubelet이 로그 루트 생성
Windows 노드에서 setupDataDirs()가 os.MkdirAll(을 호출합니다. kl.getPodLogsDir(), 0750)0750은 무시되고 디렉터리는 부모의 ACL을 상속합니다.
넓은 그룹이 권한 상속
상속된 항목 때문에 BUILTIN\Users는 파드 로그 디렉터리와 런타임이 그 안에 쓰는 파일을 읽을 수 있고, NT AUTHORITY\Authenticated Users는 수정할 수 있습니다.
다른 워크로드의 로그 열람
노드의 권한 낮은 로컬 계정이나 hostPath로 로그 경로를 마운트한 워크로드가 다른 파드의 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() - ✓신뢰할 수 있는 로그 에이전트가 아니라면 워크로드에 hostPath로
C:\var\log등 노드 로그 경로를 마운트하지 마세요. - ✓컨테이너의 stdout과 stderr에 비밀 정보를 남기지 말고 애플리케이션 로그에서 토큰을 가리세요. 노드 로그 디렉터리는 파드 자신보다 더 많은 주체가 읽을 수 있습니다.