LabHub
배우기 러닝패스 코스

쿠버네티스 배포판 — 직접 세운다 · OpenShift 로 옮길 때: 임의 UID 와 SCC · 실습

OpenShift 로 옮겼더니 권한 거부로 죽었다

LabHub 에서 이어서 보기

이 실습은 OpenShift 가 아니라 k3s 에서 흉내 냅니다

OpenShift 는 단일 노드로 설치해도 최소 8 vCPU·16GB 메모리·120GB 저장소를 요구해(OCP 4.21 문서) 이 VM(8GiB)에서 돌릴 수 없습니다.
그래서 VM 의 k3s v1.36.4+k3s1 위에서, OpenShift 의 restricted-v2 SCC 가 파드에 채워 줄 값(범위의 임의 UID, root 그룹, fsGroup)을
직접 넣어 같은 증상을 만듭니다. k3s 는 SCC 도 프로젝트 주석도 모르므로, 주석은 계산의 근거로만 씁니다 — 마지막 단계에서 그 사실도 확인합니다.

목표

루트 소유 디렉터리에 쓰는 이미지가 임의 UID 에서 권한 거부로 죽는 것을 재현하고, 이미지를 root 그룹·g=u 로 고쳐 범위 안의 어떤 UID 로도 뜨게 만들며,
fsGroup·root initContainer·읽기 전용 루트가 이 문제에서 각각 무엇을 하고 무엇을 못 하는지 구분합니다.

왜 중요한가

쿠버네티스에서 잘 돌던 이미지가 OpenShift 에서 처음 죽는 이유의 대부분은 사용자입니다. OpenShift 는 이미지의 USER 를 믿지 않고 프로젝트마다 할당한
큰 UID 로 컨테이너를 돌립니다. 컨테이너 탈출 취약점이 있어도 호스트에서 의미 있는 사용자가 되지 못하게 하려는 설계입니다. 그 사용자는 미리 알 수
없으니 이미지 안 파일의 "소유자" 로 맞출 수 없고, 대신 항상 속하는 root 그룹에 권한을 주는 것이 공식 지침입니다. 파드 설정으로 우회하려는
시도(fsGroup, root 로 chown 하는 initContainer)는 대부분 정책에 막히거나 이미지 디렉터리에는 효과가 없습니다.

단계

1. 네임스페이스 ocp-sim 을 만들고 라벨 pod-security.kubernetes.io/enforce=restricted·pod-security.kubernetes.io/warn=restricted, 주석 openshift.io/sa.scc.uid-range=1000680000/10000·openshift.io/sa.scc.supplemental-groups=1000680000/10000 을 붙이세요. 그다음 /root/ocp/project.jsonuid_range(주석 값 그대로), default_uid(restricted-v2 의 MustRunAsRange 가 기본으로 고를 UID), max_uid(범위의 마지막 UID)를 적으세요.
2. /root/ocp/legacy.yaml 로 파드 legacy 를 만드세요. 이미지 localhost/ocp-app:legacy(VM 에 미리 넣어 둠), 파드 securityContext 는 runAsNonRoot true·runAsUser 는 1단계의 default_uid·runAsGroup 0·fsGroup 1000680000·seccompProfile RuntimeDefault, 컨테이너는 allowPrivilegeEscalation false·capabilities drop ALL·terminationMessagePolicy: FallbackToLogsOnError. 재시작이 한 번 이상 일어나면 /root/ocp/crash.jsonuid, restart_count, error(종료 메시지 중 Permission denied 가 든 줄 그대로)를 적으세요.
3. 레거시 이미지로 sleep 86400 만 하는 파드 probe 를 2단계와 같은 securityContext 로 /root/ocp/probe.yaml 에 만들어 Ready 로 두세요. 그 안에서 신원을 조사해 /root/ocp/identity.jsonuid, gid, groups(id -G 의 숫자를 정렬한 배열), whoami_ok(whoami 가 성공하는지), home(HOME 환경 변수 값), home_writable(그 HOME 에 쓸 수 있는지)을 적으세요.
4. /root/ocp/app/Containerfile 을 작성해 레거시 이미지와 같은 베이스·스크립트로, /app 을 root 그룹(GID 0) 소유로 바꾸고 그룹 권한을 소유자 권한과 같게(g=u) 맞춘 뒤 숫자 USER(0 이 아닌 값)를 지정하세요. buildah 로 localhost/ocp-app:fixed 를 빌드하고 k3s 의 containerd(k8s.io 네임스페이스)에 넣은 다음, /root/ocp/build.jsontool, image, image_id(k3s crictl inspecti 의 status.id), user(이미지 설정의 User)를 적으세요.
5. 고친 이미지로 파드 fixed-a(runAsUser 는 default_uid)와 fixed-b(runAsUser 는 max_uid)를 2단계와 같은 securityContext 로 /root/ocp/fixed-a.yaml·/root/ocp/fixed-b.yaml 에 만들어 둘 다 Ready 로 두세요. /root/ocp/arbitrary.json 에 파드 이름을 키로 uid(컨테이너 안 id -u)와 data(/app/data소유자UID:그룹GID:권한8진수, 예: 0:0:755 형식)를 적으세요.
6. 레거시 이미지를 고치지 않고 버티는 두 방법을 시험하세요. (1) /root/ocp/legacy-emptydir.yaml 로 파드 legacy-ed(2단계와 같되 /app/data 에 emptyDir data 마운트)를 만들어 Ready 로 둡니다. (2) /root/ocp/legacy-init.yaml 로 root(runAsUser 0) initContainer fix-perms/app/data 를 chown 하는 파드 legacy-init 을 적용해 출력을 /root/ocp/init-denied.txt 에 저장합니다. /root/ocp/alternatives.jsonfsgroup_fixed_image_dir(fsGroup 이 있는 legacy 파드가 이미지 디렉터리 쓰기에 성공했는지), emptydir_data(legacy-ed 안 /app/data그룹GID:권한8진수), root_init_admitted(legacy-init 이 받아들여졌는지)를 적으세요.
7. 고친 이미지에 readOnlyRootFilesystem: true 만 더한 파드 ro-bare/root/ocp/ro-bare.yaml 로 만들어 실패를 보고, 같은 설정에 emptyDir 을 /app/data(이름 data)와 /tmp(이름 tmp)에 마운트하고 환경 변수 HOME=/tmp 를 준 파드 fixed-ro/root/ocp/fixed-ro.yaml 로 만들어 Ready 로 두세요. /root/ocp/readonly.jsonbare_error(ro-bare 종료 메시지 중 Read-only 가 든 줄 그대로), home(fixed-ro 안 HOME), home_writable, etc_writable(fixed-ro 안 /etc 에 쓸 수 있는지)을 적으세요.
8. /root/ocp/report.jsonroot_cause(2단계 실패의 원인: image-dir-owner·missing-capability·selinux 중 하나), whoami_ok_here(이 k3s 에서 임의 UID 로 whoami 가 되는지), openshift_runtime_adds_passwd_entry(OpenShift 문서가 CRI-O 가 임의 UID 를 /etc/passwd 에 넣어 준다고 하는지), fsgroup_fixes_image_dir, k3s_applies_uid_range(이 k3s 가 runAsUser 없는 파드에 주석의 UID 를 채워 넣는지), uid_range_default(주석으로 계산한 기본 UID), running_uids(지금 Ready 인 fixed-a·fixed-b 의 UID 를 정렬한 배열)를 적으세요.

참고

단계 8개

  1. OpenShift 프로젝트처럼 네임스페이스를 꾸민다
  2. OpenShift 로 옮겼더니 권한 거부로 죽었다
  3. /etc/passwd 에 없는 사용자로 산다는 것
  4. 이미지 쪽에서 고친다: root 그룹과 g=u
  5. 범위 안의 어떤 UID 로도 뜨는가
  6. fsGroup 과 root initContainer 는 왜 답이 아닌가
  7. 읽기 전용 루트에서 쓸 곳만 열어 준다
  8. 옮기기 전에 이미지를 점검하는 목록