본문 바로가기
IT/Proxmox

[Kubernetes] 사용자 네임스페이스 GA: UID/GID 매핑으로 컨테이너 보안 강화하기

by 수누다 2026. 4. 12.

드디어 GA! Kubernetes 사용자 네임스페이스, 이제 진지하게 써볼 때가 됐습니다

솔직히 말씀드리면, 저는 Kubernetes 사용자 네임스페이스(User Namespaces) 기능이 알파 단계일 때부터 계속 눈여겨보고 있었거든요. 근데 알파, 베타를 거치면서 "언제쯤 안정적으로 쓸 수 있을까" 하고 기다렸는데, Kubernetes 1.36에서 드디어 GA(General Availability, 정식 출시)가 됐습니다. 🎉

혹시 이런 경험 있으신가요? 컨테이너 안에서 root로 프로세스가 돌고 있는데, 만약 그 컨테이너가 탈출(escape)되면 어떻게 될까... 하는 불안감. 저도 처음 쿠버네티스를 운영하기 시작했을 때 이 부분이 항상 마음에 걸렸습니다. Kubernetes 사용자 네임스페이스는 바로 이 문제를 해결해주는 기능이에요.

오늘은 User Namespaces가 뭔지, 왜 중요한지, 그리고 실제로 어떻게 설정하는지 제 홈랩 경험을 바탕으로 풀어드리겠습니다. k8s 보안을 강화하고 싶은 분들이라면 끝까지 읽어보세요!

▲ Kubernetes 사용자 네임스페이스가 호스트와 컨테이너 사이에서 UID/GID를 어떻게 격리하는지 보여주는 전체 아키텍처 개요

Kubernetes 사용자 네임스페이스란 뭔가요? (개념 이해)

리눅스 네임스페이스를 먼저 알아야 해요

User Namespaces를 이해하려면 리눅스 네임스페이스(Linux Namespace) 개념부터 짚고 넘어가야 해요. 쉽게 말해, 리눅스 네임스페이스는 프로세스가 "자기만의 세상"을 가질 수 있게 해주는 커널 기능입니다. 컨테이너가 동작하는 기반 기술이기도 하죠.

네임스페이스에는 여러 종류가 있는데요:

  • PID Namespace: 프로세스 ID 격리
  • Network Namespace: 네트워크 인터페이스 격리
  • Mount Namespace: 파일시스템 마운트 격리
  • User Namespace (사용자 네임스페이스): UID/GID(사용자/그룹 ID) 격리 ← 오늘의 주인공!

핵심 개념: UID/GID 매핑

User Namespaces의 핵심은 UID/GID 매핑입니다. 컨테이너 안에서는 root(UID 0)처럼 보이지만, 호스트 입장에서는 전혀 다른 일반 사용자 UID로 동작하게 만드는 거예요.

예를 들어볼게요. 컨테이너 안에서 UID 0(root)으로 실행되는 프로세스가 있다고 치면:

  • User Namespaces 없을 때: 컨테이너 탈출 시 호스트에서도 UID 0(진짜 root) → 🚨 대참사
  • User Namespaces 있을 때: 컨테이너 탈출해도 호스트에서는 UID 65536 같은 일반 사용자 → 피해 최소화

이게 바로 컨테이너 격리(Container Isolation)를 한 단계 더 강화하는 방식이에요. 저도 처음엔 "그냥 securityContext에서 runAsNonRoot 설정하면 되는 거 아닌가?" 싶었는데, 그것만으로는 부족하더라고요. User Namespaces는 커널 수준에서 격리를 보장해줍니다.

Kubernetes 1.36에서 GA가 된 의미

Kubernetes 1.25에서 알파로 처음 등장했고, 1.30에서 베타, 그리고 1.36에서 GA가 됐습니다. GA가 됐다는 건 API가 안정화됐고, 프로덕션 환경에서 안심하고 써도 된다는 공식 신호거든요. 그동안 베타 때 몇 가지 제약사항이 있었는데, GA에서 많이 해소됐습니다.

버전 단계 주요 변경사항
Kubernetes 1.25 Alpha 최초 도입, 기본 비활성화, 실험적 사용
Kubernetes 1.28 Beta 기본 활성화, stateless Pod 지원
Kubernetes 1.30 Beta (개선) 볼륨 지원 범위 확대
Kubernetes 1.36 GA ✅ API 안정화, 프로덕션 권장

왜 지금 이게 중요한가요? (k8s 보안 관점)

현실적인 이야기를 해볼게요. CVE(Common Vulnerabilities and Exposures, 공통 취약점 및 노출) 목록을 보면 컨테이너 런타임이나 쿠버네티스 관련 취약점이 꾸준히 나오고 있어요. 특히 컨테이너 탈출(Container Escape) 류의 취약점이 무서운 이유는, 컨테이너 안에서 root 권한을 가진 프로세스가 호스트 root까지 영향을 줄 수 있기 때문입니다.

User Namespaces는 이런 공격의 "폭발 반경(Blast Radius)"을 줄여주는 역할을 해요. 완벽한 해결책은 아니지만, 심층 방어(Defense in Depth) 전략의 중요한 레이어 중 하나가 됩니다.

제가 운영하는 홈랩에서도 이번 GA 이후 모든 새 워크로드에 User Namespaces를 기본 적용하기로 했거든요. 삽질도 좀 했지만 ㅎㅎ 그 내용도 뒤에서 공유할게요.

실전 구현: User Namespaces 설정하기

사전 요구사항 확인

먼저 환경 체크부터 하셔야 해요. 아래 조건이 충족되어야 합니다:

  1. Kubernetes 1.28 이상 (GA는 1.36 이상 권장)
  2. 리눅스 커널 6.3 이상 (idmap mounts 지원 필요)
  3. 컨테이너 런타임 지원: containerd 1.7+, CRI-O 1.25+
  4. 노드의 /proc/sys/kernel/unprivileged_userns_clone 설정 확인 (일부 배포판)

커널 버전 확인은 이렇게 하시면 됩니다:

# 노드 커널 버전 확인
uname -r

# 예상 출력
6.8.0-51-generic

containerd 버전 확인:

containerd --version

# 예상 출력
containerd containerd.io 1.7.24 ...
Kubernetes containerd 런타임과 API 서버 User Namespaces 설정 흐름 다이어그램

▲ containerd 런타임과 Kubernetes API 서버 사이에서 User Namespaces가 활성화되는 설정 흐름 다이어그램

Pod 스펙에 User Namespaces 적용하기

설정 자체는 생각보다 간단해요. Pod 스펙의 spec.hostUsers 필드를 false로 설정하면 됩니다. 기본값은 true(User Namespaces 비활성화)이에요.

기본적인 예제부터 보시죠:

apiVersion: v1
kind: Pod
metadata:
  name: userns-demo
  namespace: default
spec:
  hostUsers: false  # 👈 핵심! User Namespaces 활성화
  containers:
  - name: app
    image: nginx:alpine
    securityContext:
      runAsUser: 0       # 컨테이너 안에서는 root처럼 동작
      runAsGroup: 0
    ports:
    - containerPort: 80

이 Pod를 배포하면, 컨테이너 안에서는 UID 0(root)이지만 호스트에서는 다른 UID로 매핑됩니다. 실제로 확인해볼게요:

# Pod 배포
kubectl apply -f userns-demo.yaml

# Pod 안에서 현재 사용자 확인
kubectl exec -it userns-demo -- id
# 출력: uid=0(root) gid=0(root) groups=0(root)  ← 컨테이너 안에서는 root!

# 호스트에서 해당 프로세스의 실제 UID 확인
# 워커 노드에서 실행
ps aux | grep nginx
# 출력: 65536 ... nginx ...  ← 호스트에서는 일반 사용자!

이거 처음 확인했을 때 진짜 신기했어요. 컨테이너 안에서는 분명히 root인데, 호스트에서 보면 65536번 사용자라니! 🎉

Deployment에 적용하는 실전 예제

실제 운영 환경에서는 Pod보다 Deployment를 주로 쓰시죠. 아래처럼 적용하시면 됩니다:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: secure-webapp
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: secure-webapp
  template:
    metadata:
      labels:
        app: secure-webapp
    spec:
      hostUsers: false  # User Namespaces 활성화
      securityContext:
        seccompProfile:
          type: RuntimeDefault  # seccomp 프로파일도 함께 적용 권장
      containers:
      - name: webapp
        image: myapp:latest
        securityContext:
          allowPrivilegeEscalation: false  # 권한 상승 차단
          readOnlyRootFilesystem: true      # 루트 파일시스템 읽기 전용
          capabilities:
            drop:
            - ALL  # 모든 Linux Capability 제거
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"

💡 : User Namespaces는 단독으로 쓰기보다 위 예제처럼 다른 보안 설정들과 함께 쓸 때 효과가 극대화됩니다. allowPrivilegeEscalation: false, capabilities drop ALL, seccomp 프로파일 적용을 묶어서 쓰는 걸 강력 추천드려요.

UID/GID 매핑 범위 확인하기

Kubernetes는 기본적으로 각 Pod에 65536개의 UID/GID 범위를 할당합니다. 실제로 어떻게 매핑되는지 확인하는 방법이에요:

# Pod가 실행 중인 노드에서 확인
# 컨테이너 PID 찾기
kubectl get pod userns-demo -o jsonpath='{.status.containerStatuses[0].containerID}'

# crictl로 컨테이너 정보 확인
crictl inspect <container-id> | grep -A 10 'uidMappings'

# 또는 /proc에서 직접 확인
cat /proc/<nginx-pid>/uid_map
# 출력 예시:
# 0  65536  65536
# (컨테이너 UID 0부터 65536개) → (호스트 UID 65536부터 매핑)

⚠️ 주의사항과 트러블슈팅: 삽질 경험 공유

볼륨 권한 문제 - 제일 많이 겪는 문제

솔직히 말씀드리면, User Namespaces 처음 적용할 때 제일 골치 아팠던 게 볼륨 권한 문제였어요. 컨테이너 안에서 UID 1000으로 파일을 쓰려고 하는데 Permission Denied가 뜨는 거예요.

원인은 이렇습니다. 호스트의 PersistentVolume 파일들이 특정 UID로 소유되어 있는데, User Namespaces가 적용되면 컨테이너의 UID와 호스트의 실제 UID가 다르게 매핑되거든요.

해결 방법 두 가지를 소개할게요:

방법 1: fsGroup 설정 활용

spec:
  hostUsers: false
  securityContext:
    fsGroup: 1000  # 볼륨 파일 그룹 소유권 설정
    fsGroupChangePolicy: "OnRootMismatch"  # 필요할 때만 변경
  containers:
  - name: app
    volumeMounts:
    - name: data
      mountPath: /data
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: my-pvc

방법 2: initContainer로 권한 설정

spec:
  hostUsers: false
  initContainers:
  - name: init-permissions
    image: busybox
    command: ['sh', '-c', 'chown -R 1000:1000 /data']
    volumeMounts:
    - name: data
      mountPath: /data
  containers:
  - name: app
    # ...

hostNetwork, hostPID와 함께 쓸 수 없어요

hostNetwork: true 또는 hostPID: true를 사용하는 Pod에는 User Namespaces를 적용할 수 없습니다. 이건 설계상의 제약이에요. 호스트 네트워크/PID를 공유하면서 사용자 네임스페이스를 격리하는 건 서로 상충되거든요.

# 이런 설정은 에러가 납니다!
spec:
  hostUsers: false
  hostNetwork: true  # ❌ 함께 사용 불가!

만약 이렇게 설정하면 Pod validation failed: spec.hostUsers: Invalid value 같은 에러를 보게 됩니다. 저도 처음에 이거 때문에 한참 헤맸어요 ㅎㅎ

일부 이미지는 root 권한이 실제로 필요할 수 있어요

몇몇 레거시 이미지들은 실제로 호스트 수준의 root 권한을 필요로 하는 경우가 있어요. 이런 경우 User Namespaces를 적용하면 동작하지 않을 수 있습니다. 적용 전에 반드시 스테이징 환경에서 테스트를 먼저 해보세요.

커널 버전 확인 필수!

이건 정말 중요한 포인트예요. 커널 5.x 버전대에서는 idmap mounts가 제대로 지원이 안 돼서 볼륨 관련 기능에 제약이 있었어요. 커널 6.3 이상을 강력히 권장합니다. Ubuntu 22.04 LTS 기준으로는 HWE(Hardware Enablement) 커널을 사용하면 6.x 버전을 쓸 수 있어요.

검증: 실제로 보안이 강화됐는지 확인하기

UID 매핑 확인

설정이 제대로 됐는지 확인하는 방법입니다. 이 과정을 꼭 거치시는 걸 추천드려요:

# 1. Pod 상태 확인
kubectl get pod userns-demo -o yaml | grep hostUsers
# 출력: hostUsers: false  ✅

# 2. 컨테이너 안에서 UID 확인
kubectl exec -it userns-demo -- id
# 출력: uid=0(root) gid=0(root)  ← 컨테이너 안에서는 root

# 3. 호스트에서 실제 UID 확인 (워커 노드에서)
crictl ps | grep userns-demo
crictl inspect <container-id> | python3 -m json.tool | grep -A5 uidMappings
# 출력 예시:
# "uidMappings": [
#   {
#     "containerID": 0,
#     "hostID": 65536,  ← 호스트에서는 65536!
#     "size": 65536
#   }
# ]

간단한 보안 검증 테스트

# 컨테이너 안에서 프로세스 상태 확인
kubectl exec -it userns-demo -- cat /proc/1/status | grep -E 'Uid|Gid'
# 컨테이너 안:
# Uid: 0 0 0 0  ← 컨테이너 기준 root

# 호스트에서 같은 프로세스 확인
cat /proc/<host-pid>/status | grep -E 'Uid|Gid'
# 호스트 기준:
# Uid: 65536 65536 65536 65536  ← 호스트 기준 일반 사용자!

이 차이가 User Namespaces의 핵심이에요. 컨테이너가 탈출해도 호스트에서는 UID 65536의 권한만 가지게 됩니다. 드디어 됐다! 싶은 순간이에요 😄

Kubernetes User Namespaces 적용 전후 UID 매핑 보안 검증 결과 비교 대시보드

▲ User Namespaces 적용 전후 UID 매핑 비교 및 호스트에서 확인한 실제 프로세스 권한 검증 결과

User Namespaces와 다른 보안 기능 비교 정리

자주 헷갈려하시는 부분인데요, User Namespaces가 다른 보안 기능들과 어떻게 다른지 정리해드릴게요:

보안 기능 보호 방식 적용 레벨 함께 사용?
User Namespaces UID/GID 격리, 컨테이너 탈출 시 피해 최소화 커널 수준 ✅ 권장
runAsNonRoot 컨테이너 내 root 실행 방지 컨테이너 수준 ✅ 권장
Seccomp 시스템 콜 필터링 커널 수준 ✅ 권장
AppArmor/SELinux MAC(강제 접근 제어) 커널 수준 ✅ 권장
capabilities drop Linux 권한 제거 프로세스 수준 ✅ 권장
PodSecurityAdmission Pod 보안 정책 적용 API 수준 ✅ 통합 관리

이 기능들은 서로 대체 관계가 아니라 보완 관계입니다. 심층 방어 전략에서는 여러 레이어를 함께 쓰는 게 정석이에요.

Kubernetes 컨테이너 보안 User Namespaces Seccomp AppArmor 다중 레이어 보안 전략 인포그래픽

▲ Kubernetes 컨테이너 보안 강화를 위한 User Namespaces, Seccomp, AppArmor 등 다중 레이어 보안 전략 비교 인포그래픽

자주 묻는 질문 (FAQ)

Q. 기존 워크로드에 갑자기 적용해도 되나요?

A. 절대 갑자기 적용하지 마세요. 반드시 스테이징 환경에서 먼저 테스트하고, 볼륨 권한 문제나 애플리케이션 동작 이상이 없는지 확인한 후에 단계적으로 적용하시길 권장합니다.

Q. 성능 오버헤드는 없나요?

A. 실제로 측정해보니 거의 무시할 수준이었어요. UID/GID 매핑은 커널에서 효율적으로 처리되기 때문에 일반적인 워크로드에서는 체감하기 어렵습니다.

Q. 모든 CSI 드라이버가 지원되나요?

A. 아직 모든 CSI 드라이버가 idmap mounts를 지원하지는 않아요. 사용 중인 스토리지 드라이버가 지원하는지 먼저 확인하세요. NFS 볼륨의 경우 별도 설정이 필요할 수 있습니다.

Q. 윈도우 노드에서도 되나요?

A. 아니요. User Namespaces는 리눅스 커널 기능이라 윈도우 노드에서는 지원되지 않습니다.

마무리: 이제 User Namespaces를 기본값으로 생각하세요

Kubernetes 사용자 네임스페이스가 GA가 된 지금, 저는 이 기능을 "있으면 좋은 것"이 아니라 "새 워크로드의 기본 보안 설정"으로 봐야 한다고 생각해요.

물론 처음 적용할 때 볼륨 권한 문제나 호환성 이슈로 삽질을 좀 할 수 있어요. 저도 그랬으니까요 ㅎㅎ. 근데 그 삽질이 결국 더 안전한 클러스터를 만들어 주거든요. 특히 멀티테넌트 환경이나 외부 서비스를 운영하는 분들이라면 더더욱 적용을 권장합니다.

오늘 배운 내용을 정리하면:

  • ✅ User Namespaces는 컨테이너 안 UID와 호스트 UID를 분리해서 컨테이너 탈출 시 피해를 최소화
  • spec.hostUsers: false 한 줄로 활성화 가능
  • ✅ 커널 6.3+, containerd 1.7+, Kubernetes 1.28+ 환경 필요
  • ✅ 다른 보안 기능들(seccomp, capabilities drop 등)과 함께 쓸 것
  • ⚠️ 볼륨 권한 문제는 사전에 꼭 테스트하고 적용
  • ⚠️ hostNetwork/hostPID와는 함께 사용 불가

다음 글에서는 PodSecurityAdmission(PSA)과 User Namespaces를 조합해서 클러스터 전체에 보안 정책을 강제 적용하는 방법을 다룰 예정입니다. 관심 있으신 분들은 기대해 주세요!

궁금한 점이나 직접 해보시다가 막히는 부분 있으시면 댓글로 남겨주세요. 같이 고민해봐요! 😊