목록으로

Programming Notes

SELinux 볼륨 레이블 변경 사항 GA 적용 (v1.37에 미칠 영향)

SELinux를 'Enforcing(강제)' 모드로 설정한 Linux에서 Kubernetes를 실행 중이라면 미리 계획을 세우세요. 향후 릴리스(v1.37 예정)에서는 SELinuxMount 피처 게이트(Feature Gate)가 기본적으로 활성화될 예정입니다. 이 기능은 대부분의 워크로드에서 볼륨 설정 속도를 높여주지만, 여전히 이전의 재귀적 레이블링(Recursive Relabeling) 모델에 의존하는 애플리케이션에는 문제가 발생할 수 있습니다 (예: 동일한 노드에서 권한이 있는 Pod와 없는 Pod 간에 하나의 볼륨을 공유하는 경우). Kubernetes v1.36은 클러스터를 점검하고 이러한 변경 사항을 수정하거나 거부(Opt-out)하기에 적기입니다.

노드에서 SELinux를 사용하지 않는다면 변경 사항이 없습니다. SELinux가 비활성화되어 있거나 Linux 커널에서 사용할 수 없는 경우, kubelet은 모든 SELinux 로직을 건너뜁니다. 이 글은 읽지 않으셔도 무방합니다.

이 블로그는 SELinuxMountReadWriteOncePod 피처 게이트에 대해 설명했던 이전 글 Kubernetes 1.27: 효율적인 SELinux 레이블링(Beta)을 기반으로 작성되었습니다. 해결하려는 문제는 동일하지만, 이번 블로그에서는 그 범위를 모든 볼륨으로 확장합니다.

문제점

SELinux(Security Enhanced Linux)가 활성화된 Linux 시스템은 객체(예: 파일 및 네트워크 소켓)에 부착된 레이블을 사용하여 액세스 제어를 결정합니다. 역사적으로 컨테이너 런타임은 Pod와 그 모든 볼륨에 SELinux 레이블을 적용해 왔습니다. Kubernetes는 Pod의 securityContext 필드에 있는 SELinux 레이블을 컨테이너 런타임에 전달하기만 합니다.

컨테이너 런타임은 Pod의 컨테이너에 표시되는 모든 파일의 SELinux 레이블을 재귀적으로 변경합니다. 볼륨에 파일이 많거나 원격 파일 시스템인 경우 이 작업은 상당한 시간이 걸릴 수 있습니다.

주의:

컨테이너가 볼륨의 subPath를 사용하는 경우, 전체 볼륨 중 해당 subPath만 다시 레이블링됩니다. 이를 통해 서로 다른 SELinux 레이블을 가진 두 Pod가 볼륨의 서로 다른 하위 경로를 사용하는 한 동일한 볼륨을 공유할 수 있습니다.

Pod에 지정된 SELinux 레이블이 없는 경우, 컨테이너 런타임은 임의의 레이블을 할당하여 컨테이너 경계를 벗어난 프로세스가 호스트의 다른 컨테이너 데이터에 접근하지 못하도록 합니다. 이 경우에도 컨테이너 런타임은 모든 Pod 볼륨에 이 임의의 레이블을 재귀적으로 적용합니다.

Kubernetes의 개선 사항

스택이 지원하는 경우, kubelet은 -o context=<label> 옵션으로 볼륨을 마운트할 수 있습니다. 이렇게 하면 커널이 재귀적인 inode 순회 없이 해당 마운트의 모든 inode에 올바른 레이블을 적용합니다. 이 경로는 피처 플래그에 의해 제어되며, Pod가 충분한 SELinux 레이블(예: spec.securityContext.seLinuxOptions.level)을 노출하고 볼륨 드라이버가 이를 허용(CSI의 경우 CSIDriver 필드 spec.seLinuxMount: true)해야 합니다.

Kubernetes 프로젝트는 이를 단계적으로 도입했습니다:

  • ReadWriteOncePod 볼륨SELinuxMountReadWriteOncePod 피처 게이트를 통해 처리되었으며, v1.28부터 기본으로 활성화되었고 v1.36에서 GA가 되었습니다.
  • 더 넓은 범위의 지원SELinuxMount 플래그와 Pod의 spec.securityContext.seLinuxChangePolicy 필드를 통해 처리됩니다.

Pod와 볼륨이 다음 모든 조건을 충족하면, Kubernetes는 올바른 SELinux 레이블로 볼륨을 직접 마운트합니다. 이러한 마운트는 일정한 시간(constant time) 내에 완료되며, 컨테이너 런타임이 파일들을 재귀적으로 다시 레이블링할 필요가 없습니다. 이를 위해서는 다음이 필요합니다:

  1. 운영체제가 SELinux를 지원해야 합니다. SELinux 지원이 감지되지 않으면 kubelet과 컨테이너 런타임은 SELinux와 관련된 어떤 작업도 수행하지 않습니다.

  2. SELinuxMountReadWriteOncePod 피처 게이트가 활성화되어야 합니다. Kubernetes v1.36을 사용 중이라면 이 기능은 무조건 활성화되어 있습니다.

  3. Pod가 적절한 accessModes를 가진 PersistentVolumeClaim을 사용해야 합니다.

    • 볼륨이 accessModes: ["ReadWriteOncePod"]를 사용하거나,
    • SELinuxChangePolicySELinuxMount 피처 게이트가 모두 활성화되어 있고, Pod의 spec.securityContext.seLinuxChangePolicy가 nil(기본값) 또는 MountOption으로 설정된 경우 모든 액세스 모드를 사용할 수 있습니다.

    SELinuxMount 피처 게이트는 Kubernetes 1.36에서 베타(Beta) 상태이며 기본적으로 비활성화되어 있습니다. 다른 모든 SELinux 관련 피처 게이트는 현재 정식 버전(GA)입니다.

    이러한 피처 게이트 중 하나라도 비활성화되면, SELinux 레이블은 항상 컨테이너 런타임에 의해 볼륨(또는 하위 경로)을 재귀적으로 순회하며 적용됩니다.

  4. Pod의 보안 컨텍스트(security context)에 최소한 seLinuxOptions.level이 지정되어야 합니다. 또는 해당 Pod의 모든 컨테이너가 컨테이너 레벨의 보안 컨텍스트에 이를 설정해야 합니다. Kubernetes는 운영체제 기본값(일반적으로 system_u, system_r, container_t)에서 기본 user, role, type을 읽어옵니다.

    Kubernetes가 최소한 SELinux level을 알지 못하면 컨테이너 런타임은 볼륨이 마운트된 후 임의의 레벨을 할당합니다. 이 경우에도 컨테이너 런타임은 볼륨을 재귀적으로 다시 레이블링합니다.

  5. 해당 볼륨을 담당하는 볼륨 플러그인 또는 CSI 드라이버가 SELinux 마운트 옵션을 지원해야 합니다.

    기본 내장(in-tree) 볼륨 플러그인 중 fciscsi는 SELinux 마운트 옵션을 지원합니다.

    SELinux 마운트 옵션을 지원하는 CSI 드라이버는 CSIDriver 인스턴스에서 seLinuxMount 필드를 true로 설정하여 이 기능을 선언해야 합니다.

    seLinuxMount: true로 설정되지 않은 다른 볼륨 플러그인이나 CSI 드라이버가 관리하는 볼륨은 컨테이너 런타임에 의해 재귀적으로 레이블이 재지정됩니다.

파괴적 변경 사항 (Breaking Change)

SELinuxMount 피처 게이트는 여러 Pod 간에 볼륨을 공유할 수 있는 방식을 미묘하게 변경합니다.

재귀적 레이블링 방식에서는 다음의 두 가지 경우가 모두 작동합니다:

  1. 서로 다른 SELinux 레이블을 가진 두 Pod가 동일한 볼륨을 공유하지만, 각각 볼륨의 다른 subPath를 사용하는 경우.
  2. 권한이 있는 Pod(Privileged)와 권한이 없는 Pod가 동일한 볼륨을 공유하는 경우.

위 시나리오들은 SELinux가 활성화된 Kubernetes의 최신 마운트 동작에서는 작동하지 않습니다. 대신, 한 Pod가 종료될 때까지 다른 Pod 중 하나는 ContainerCreating 상태로 멈추게 됩니다.

첫 번째 사례는 매우 드물며 실제로 거의 발견되지 않았습니다. 두 번째 사례 역시 드물긴 하지만 실제 애플리케이션에서 관찰되었습니다. Kubernetes v1.36은 이러한 Pod를 식별할 수 있는 메트릭과 이벤트를 제공하며, 클러스터 관리자가 Pod 필드인 spec.securityContext.seLinuxChangePolicy를 통해 마운트 옵션을 거부(opt-out)할 수 있도록 허용합니다.

seLinuxChangePolicy

새로운 Pod 필드인 spec.securityContext.seLinuxChangePolicy는 모든 Pod 볼륨에 SELinux 레이블을 적용하는 방식을 지정합니다. Kubernetes v1.36에서 이 필드는 안정적인(Stable) Pod API의 일부입니다.

사용 가능한 세 가지 선택지는 다음과 같습니다:

  • 필드가 설정되지 않음 (기본값): Kubernetes v1.36에서는 SELinuxMount 피처 게이트의 활성화 여부에 따라 동작이 달라집니다. 기본적으로 해당 피처 게이트는 활성화되지 않으므로 SELinux 레이블이 재귀적으로 적용됩니다. 클러스터에서 이 피처 게이트를 활성화하고 다른 모든 조건이 충족되면 마운트 옵션을 사용하여 레이블링이 적용됩니다.
  • Recursive: SELinux 레이블이 재귀적으로 적용됩니다. 이는 마운트 옵션 사용을 거부(opt-out)하는 것입니다.
  • MountOption: 다른 모든 조건이 충족되는 경우 마운트 옵션을 사용하여 SELinux 레이블이 적용됩니다. 이 선택지는 SELinuxMount 피처 게이트가 활성화된 경우에만 사용할 수 있습니다.

SELinux 경고 컨트롤러 (선택 사항)

Kubernetes v1.36은 컨트롤 플레인 내에 새로운 컨트롤러인 selinux-warning-controller를 제공합니다. 이 컨트롤러는 kube-controller-manager 내에서 실행됩니다. 이를 사용하려면 kube-controller-manager 명령줄에 --controllers=*,selinux-warning-controller를 추가해야 하며, SELinuxChangePolicy 피처 게이트가 명시적으로 비활성화되어 있지 않아야 합니다.

이 컨트롤러는 클러스터의 모든 Pod를 모니터링하고, SELinuxMount 피처 게이트와 호환되지 않는 방식으로 동일한 볼륨을 공유하는 두 Pod를 발견하면 이벤트를 발생시킵니다. 충돌하는 모든 Pod는 다음과 같은 이벤트를 받게 됩니다:

SELinuxLabel "system_u:system_r:container_t:s0:c98,c99" conflicts with pod my-other-pod that uses the same volume as this pod with SELinuxLabel "system_u:system_r:container_t:s0:c0,c1". If both pods land on the same node, only one of them may access the volume.

네임스페이스 경계를 넘어 정보가 유출되는 것을 방지하기 위해 충돌하는 Pod가 다른 네임스페이스에 있는 경우 실제 Pod 이름은 가려질 수 있습니다.

이 컨트롤러는 스케줄러의 결정과 상관없이 모든 Pod가 정상적으로 작동하도록 보장하기 위해, 두 Pod가 동일한 노드에 있지 않더라도 이러한 이벤트를 보고합니다. 다음번에는 같은 노드에서 실행될 수도 있기 때문입니다.

또한, 컨트롤러는 현재 발생한 모든 Pod 간 충돌을 나열하는 selinux_warning_controller_selinux_volume_conflict 메트릭을 내보냅니다. 이 메트릭에는 충돌하는 Pod와 해당 SELinux 레이블을 식별하는 레이블이 포함됩니다:

selinux_warning_controller_selinux_volume_conflict{pod1_name="my-other-pod",pod1_namespace="default",pod1_value="system_u:object_r:container_file_t:s0:c0,c1",pod2_name="my-pod",pod2_namespace="default",pod2_value="system_u:object_r:container_file_t:s0:c0,c2",property="SELinuxLabel"} 1

이 선택적 컨트롤러를 활성화할 때 보안상 고려할 점이 있습니다. 메트릭에 네임스페이스 이름이 항상 포함되므로 이를 노출할 수 있습니다. Kubernetes 프로젝트는 클러스터 관리자만이 kube-controller-manager 메트릭에 접근할 수 있다고 가정합니다.

권장 업그레이드 경로

v1.36에서 SELinuxMount가 활성화되는 릴리스(v1.37 예정)로의 원활한 업그레이드를 위해 다음 단계를 따르는 것이 좋습니다:

  1. kube-controller-manager에서 selinux-warning-controller를 활성화합니다.
  2. selinux_warning_controller_selinux_volume_conflict 메트릭을 확인합니다. 이는 Pod 간의 모든 잠재적인 충돌을 보여줍니다. 충돌하는 각 Pod(Deployment, StatefulSet 등)에 대해 거부 설정(Pod의 spec.securityContext.seLinuxChangePolicy: Recursive 설정)을 적용하거나, 충돌을 제거하도록 애플리케이션 구조를 변경하세요. 예를 들어, 해당 Pod가 정말로 권한이 있는(privileged) 상태로 실행되어야 하는지 검토하세요.
  3. volume_manager_selinux_volume_context_mismatch_warnings_total 메트릭을 확인합니다. 이 메트릭은 SELinuxMount가 비활성화된 상태에서 실행되지만 활성화되면 시작되지 않을 Pod가 있을 때 kubelet에 의해 생성됩니다. 이 메트릭은 실제 충돌을 겪게 될 Pod의 수를 보여줍니다. 아쉽게도 이 메트릭은 레이블에 정확한 Pod 이름을 노출하지 않습니다. 전체 Pod 이름은 selinux_warning_controller_selinux_volume_conflict 메트릭에서만 확인할 수 있습니다.
  4. 두 메트릭이 모두 정리되면 SELinuxMount가 활성화된 Kubernetes 버전으로 업그레이드합니다.

네임스페이스 단위나 클러스터 전체의 모든 Pod에 거부 설정을 적용하려면 MutatingAdmissionPolicy, 변조 어드미션 웹후크(mutating webhook), 또는 KyvernoGatekeeper와 같은 정책 엔진을 사용하는 것을 고려해 보세요.

SELinuxMount가 활성화되면, kubelet은 동일한 볼륨을 사용하는 기존 Pod와 SELinux 레이블이 충돌하여 시작되지 못한 Pod의 수를 volume_manager_selinux_volume_context_mismatch_errors_total 메트릭으로 내보냅니다. selinux-warning-controller가 활성화되어 있다면 정확한 Pod 이름은 여전히 selinux_warning_controller_selinux_volume_conflict 메트릭에서 확인할 수 있습니다.

추가 읽을거리

감사의 글

문제가 발생하거나 의견이 있거나 기여하고 싶다면 Kubernetes Slack의 #sig-node#sig-storage 채널을 방문하거나 SIG Node 또는 SIG Storage 회의에 참여해 주세요.