쿠버네티스 배포판 — 직접 세운다 · OpenShift 로 옮길 때: 임의 UID 와 SCC · 실습
OpenShift 로 옮겼더니 권한 거부로 죽었다
이 실습은 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.json 에 uid_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.json 에 uid, restart_count, error(종료 메시지 중 Permission denied 가 든 줄 그대로)를 적으세요.
3. 레거시 이미지로 sleep 86400 만 하는 파드 probe 를 2단계와 같은 securityContext 로 /root/ocp/probe.yaml 에 만들어 Ready 로 두세요. 그 안에서 신원을 조사해 /root/ocp/identity.json 에 uid, 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.json 에 tool, 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.json 에 fsgroup_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.json 에 bare_error(ro-bare 종료 메시지 중 Read-only 가 든 줄 그대로), home(fixed-ro 안 HOME), home_writable, etc_writable(fixed-ro 안 /etc 에 쓸 수 있는지)을 적으세요.
8. /root/ocp/report.json 에 root_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 를 정렬한 배열)를 적으세요.
참고
- kubeconfig:
/etc/rancher/k3s/k3s.yaml. 빌드 도구는 buildah 뿐입니다(docker·podman 없음). - 이미지 옮기기:
buildah push <이미지> docker-archive:<파일>:<이름>→k3s ctr -n k8s.io images import <파일>→k3s crictl images - 종료 메시지:
kubectl -n ocp-sim get pod <이름> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.message}' - 흔한 실수: 이미지 안에서
chown 1001처럼 특정 UID 에 맞추는 것. OpenShift 는 그 UID 로 돌리지 않습니다. - 흔한 실수: 같은 태그로 다시 빌드한 뒤 k3s 에 가져오지 않는 것. 파드는 containerd 에 있는 옛 이미지를 씁니다.
- [OCP 4.21 이미지 작성 지침](https://docs.redhat.com/en/documentation/openshift_container_platform/4.21/html/images/creating-images) · [OCP 4.19 SCC 관리](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/authentication_and_authorization/managing-pod-security-policies)
단계 8개
- OpenShift 프로젝트처럼 네임스페이스를 꾸민다
- OpenShift 로 옮겼더니 권한 거부로 죽었다
- /etc/passwd 에 없는 사용자로 산다는 것
- 이미지 쪽에서 고친다: root 그룹과 g=u
- 범위 안의 어떤 UID 로도 뜨는가
- fsGroup 과 root initContainer 는 왜 답이 아닌가
- 읽기 전용 루트에서 쓸 곳만 열어 준다
- 옮기기 전에 이미지를 점검하는 목록