Certified Kubernetes Application Developer (CKAD) #15 SecurityContext와 Capabilities: runAsUser, fsGroup, readOnly rootfs

#14 ServiceAccount와 RBAC에서 컨테이너가 쿠버네티스 API에 무엇을 할 수 있는지를 제한했다면, 이번에는 그보다 한 층 아래인 컨테이너 프로세스 자체가 리눅스에서 어떤 권한으로 도는지를 제한합니다. 기본값을 그대로 두면 많은 이미지가 root로 실행되고 루트 파일시스템에 마음껏 쓸 수 있습니다. 공격자가 컨테이너를 장악하면 그 권한을 그대로 물려받습니다.

securityContext는 컨테이너가 어떤 UID/GID로 실행될지, 루트 파일시스템에 쓸 수 있는지, 어떤 리눅스 커널 기능(capability)을 가질지를 선언으로 통제하는 필드입니다. CKAD에서는 “이 컨테이너를 비루트로 실행하라”, “NET_ADMIN capability만 추가하라”, “루트 파일시스템을 읽기 전용으로 만들라” 같은 작업으로 직접 출제됩니다. 채점 스크립트가 id나 매니페스트 필드로 결과를 검사하므로, 필드 위치와 철자를 정확히 아는 것이 점수를 가릅니다.

securityContext는 두 층위에 붙는다 #

securityContextPod 레벨컨테이너 레벨 두 곳에 모두 선언할 수 있습니다. 둘은 위치도 다르고 적용 범위도 다릅니다.

위치경로적용 범위
Pod 레벨spec.securityContextPod 안 모든 컨테이너의 기본값
컨테이너 레벨spec.containers[].securityContext해당 컨테이너에만 적용

두 위치에 같은 필드가 있으면 컨테이너 레벨이 Pod 레벨을 덮어씁니다(override). 즉 Pod 레벨에서 공통 정책을 깔아 두고, 특정 컨테이너만 컨테이너 레벨에서 예외를 주는 식으로 씁니다.

apiVersion: v1
kind: Pod
metadata:
  name: ctx-demo
spec:
  securityContext:          # Pod 레벨: 모든 컨테이너 기본값
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      securityContext:      # 컨테이너 레벨: 이 컨테이너만 덮어씀
        runAsUser: 2000

위 예제에서 app 컨테이너의 프로세스는 컨테이너 레벨이 우선하므로 UID 2000으로 실행됩니다. 같은 Pod에 다른 컨테이너가 있었다면 그 컨테이너는 Pod 레벨 값인 UID 1000을 따릅니다. 한편 fsGroup처럼 컨테이너 레벨에 없는 필드(fsGroup은 Pod 레벨 전용)는 Pod 레벨 값이 그대로 적용됩니다.

또 하나 중요한 구분이 있습니다. runAsUsercapabilities처럼 일부 필드는 컨테이너 레벨에만 있고, fsGroup이나 supplementalGroups처럼 일부 필드는 Pod 레벨에만 있습니다. 어느 필드가 어느 층위에 속하는지 헷갈리면 kubectl explain으로 즉시 확인합니다.

k explain pod.spec.securityContext
k explain pod.spec.containers.securityContext

핵심 필드: 어떤 사용자로 실행할 것인가 #

가장 자주 출제되는 묶음은 실행 사용자 제어입니다.

필드층위의미
runAsUser둘 다프로세스의 UID 지정
runAsGroup둘 다프로세스의 기본 GID 지정
runAsNonRoot둘 다true면 root(UID 0) 실행을 거부
fsGroupPod마운트된 볼륨의 소유 그룹 GID
supplementalGroupsPod프로세스에 추가로 부여할 보조 GID 목록

runAsUser / runAsGroup / runAsNonRoot #

runAsUser는 컨테이너 안 메인 프로세스가 어떤 UID로 뜰지를 지정합니다. runAsNonRoot: true는 한 발 더 나아가, 이미지가 root로 뜨도록 만들어졌으면 컨테이너를 아예 시작하지 않고 실패시킵니다. 비루트 실행을 강제하고 싶을 때 가장 확실한 방어선입니다.

apiVersion: v1
kind: Pod
metadata:
  name: nonroot-demo
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "id && sleep 3600"]
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 3000

runAsNonRoot: true만 두고 runAsUser를 비워 두면, 이미지의 기본 USER가 root인 경우 컨테이너가 CreateContainerConfigError로 뜨지 않습니다. 비루트 UID를 함께 지정하는 편이 안전합니다.

fsGroup: 볼륨 소유 그룹 #

비루트로 실행하면 마운트된 볼륨에 쓰지 못하는 문제가 흔히 생깁니다. fsGroup을 지정하면 쿠버네티스가 마운트 시점에 볼륨의 그룹 소유권을 그 GID로 바꾸고, 프로세스에 그 GID를 보조 그룹으로 부여합니다. 그 결과 비루트 프로세스도 볼륨에 쓸 수 있게 됩니다.

apiVersion: v1
kind: Pod
metadata:
  name: fsgroup-demo
spec:
  securityContext:
    runAsUser: 1000
    fsGroup: 2000          # /data의 그룹 소유가 2000으로 바뀜
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "touch /data/test && ls -l /data && sleep 3600"]
      volumeMounts:
        - name: scratch
          mountPath: /data
  volumes:
    - name: scratch
      emptyDir: {}

이 Pod가 뜨면 /data의 그룹이 2000으로 설정되어, UID 1000으로 도는 프로세스가 touch에 성공합니다. supplementalGroups는 비슷하지만 볼륨 소유권을 바꾸지 않고 프로세스에 보조 GID만 추가합니다.

파일시스템 권한 통제 #

readOnlyRootFilesystem #

컨테이너 루트 파일시스템을 읽기 전용으로 만들면, 공격자가 침투해도 바이너리를 심거나 설정을 변조하기 어렵습니다. readOnlyRootFilesystem: true는 컨테이너 레벨 필드입니다.

securityContext:
  readOnlyRootFilesystem: true

문제는 많은 앱이 /tmp나 캐시 디렉터리에 쓰기를 해야 정상 동작한다는 점입니다. 이때는 루트는 읽기 전용으로 두되, 쓰기가 필요한 경로에만 emptyDir를 마운트해 우회합니다.

apiVersion: v1
kind: Pod
metadata:
  name: ro-rootfs
spec:
  containers:
    - name: app
      image: nginx:1.27
      securityContext:
        readOnlyRootFilesystem: true
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /var/cache/nginx
        - name: run
          mountPath: /var/run
  volumes:
    - name: tmp
      emptyDir: {}
    - name: cache
      emptyDir: {}
    - name: run
      emptyDir: {}

루트는 읽기 전용이지만 /tmp,/var/cache/nginx,/var/runemptyDir라 쓰기가 가능합니다. 이 패턴은 “루트 파일시스템을 읽기 전용으로 만들되 앱이 계속 동작하게 하라"는 형태로 출제됩니다.

allowPrivilegeEscalation #

allowPrivilegeEscalation: false는 프로세스가 자신보다 높은 권한을 얻는 것(예 setuid 바이너리 실행)을 막습니다. 비루트 실행과 함께 두면 권한 상승 경로를 한 번 더 닫습니다. 컨테이너 레벨 필드입니다.

securityContext:
  allowPrivilegeEscalation: false

Linux capabilities #

리눅스는 root의 권한을 잘게 쪼갠 capability 단위로 관리합니다. 컨테이너 런타임은 기본적으로 일부 capability만 부여하는데, securityContext.capabilities개별 추가(add)제거(drop) 를 할 수 있습니다. 이 필드는 컨테이너 레벨 전용입니다.

securityContext:
  capabilities:
    add: ["NET_ADMIN", "SYS_TIME"]
    drop: ["ALL"]

값은 CAP_ 접두사를 떼고 적습니다(예 CAP_NET_ADMIN이 아니라 NET_ADMIN). 보안 모범 사례는 **drop: ["ALL"]로 전부 제거한 뒤 꼭 필요한 것만 add**하는 최소 권한 방식입니다.

capability용도 예시
NET_ADMIN네트워크 인터페이스,라우팅,iptables 조작
SYS_TIME시스템 시계 변경
CHOWN파일 소유권 변경
NET_BIND_SERVICE1024 미만 포트 바인딩

drop과 add를 함께 쓰면 둘이 충돌하지 않습니다. 먼저 모두 떨어뜨린 다음 명시한 것만 다시 붙는다고 이해하면 됩니다.

apiVersion: v1
kind: Pod
metadata:
  name: cap-demo
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      securityContext:
        capabilities:
          drop: ["ALL"]
          add: ["NET_ADMIN"]

privileged: true의 위험 #

privileged: true는 컨테이너에 호스트의 거의 모든 권한을 부여합니다. 모든 capability를 켜고 디바이스 접근까지 열어, 사실상 컨테이너 격리를 무너뜨립니다. 시험이나 실무에서 명시적으로 요구하지 않는 한 절대 켜지 않습니다. CKAD에서는 “privileged를 끄라” 또는 “최소 권한으로 바꾸라"는 방향으로 나오지, 켜라고 나오는 경우는 드뭅니다.

securityContext:
  privileged: false   # 기본값이자 권장값

seccompProfile #

seccompProfile은 컨테이너가 호출할 수 있는 시스템 콜을 제한합니다. CKAD에서는 RuntimeDefault 프로파일을 거는 한 줄 정도만 알면 충분하고, 커스텀 프로파일과 상세 운영은 CKS의 영역입니다.

securityContext:
  seccompProfile:
    type: RuntimeDefault

결과 검증 #

매니페스트를 적용한 뒤에는 컨테이너 안에서 실제 실행 신원을 확인해 채점 기준과 맞는지 봅니다.

# UID/GID와 보조 그룹 확인
k exec ctx-demo -- id

# 사용자 이름 확인 (UID만 있고 이름이 없으면 숫자로 표시될 수 있음)
k exec nonroot-demo -- whoami

# 볼륨 그룹 소유권 확인
k exec fsgroup-demo -- ls -ld /data

# 읽기 전용 루트 확인 (쓰기 시도 시 실패해야 정상)
k exec ro-rootfs -c app -- touch /test 2>&1 || echo "read-only 확인"

id 출력의 uid,gid,groups가 매니페스트에 적은 값과 일치하면 적용이 끝난 것입니다. runAsNonRoot: true인데 컨테이너가 CreateContainerConfigError로 뜬다면, 이미지가 root로 실행되도록 만들어졌다는 신호이므로 비루트 UID를 명시합니다.

종합 예제 #

비루트 실행, 읽기 전용 루트 파일시스템, 최소 capability를 한 번에 적용한 매니페스트입니다. 시험에서 요구하는 “보안을 강화한 Pod"의 전형적인 형태입니다.

apiVersion: v1
kind: Pod
metadata:
  name: hardened
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000
  containers:
    - name: app
      image: nginx:1.27
      ports:
        - containerPort: 8080
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
          add: ["NET_BIND_SERVICE"]
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: run
          mountPath: /var/run
  volumes:
    - name: tmp
      emptyDir: {}
    - name: run
      emptyDir: {}

이 Pod는 비루트 UID 1000으로 뜨고, 권한 상승이 막혀 있으며, 모든 capability를 떨어뜨린 뒤 1024 미만 포트 바인딩에 필요한 NET_BIND_SERVICE만 다시 붙였습니다. 루트는 읽기 전용이지만 /tmp,/var/runemptyDir를 붙여 동작에 지장이 없습니다.

시험 포인트 #

  • securityContextPod 레벨(spec.securityContext)과 컨테이너 레벨(spec.containers[].securityContext) 두 곳에 있고, 컨테이너 레벨이 Pod 레벨을 덮어쓴다.
  • runAsUser,runAsGroup,runAsNonRoot,capabilities,readOnlyRootFilesystem,allowPrivilegeEscalation,privileged컨테이너 레벨, fsGroup,supplementalGroupsPod 레벨 전용이다.
  • capability 값은 CAP_ 접두사를 떼고 적는다. 모범 답안은 drop: ["ALL"] 후 필요한 것만 add.
  • readOnlyRootFilesystem: true로 쓰기가 막히면 emptyDir를 쓰기 경로에 마운트해 우회한다.
  • runAsNonRoot: true만 두고 비루트 UID를 안 주면 root 이미지가 시작에 실패할 수 있으니 runAsUser를 함께 지정한다.
  • 검증은 k exec -- idk exec -- whoami. 채점 전에 실제 UID/GID를 눈으로 확인한다.
  • 필드 위치가 헷갈리면 k explain pod.spec.securityContextk explain pod.spec.containers.securityContext로 즉시 확인한다.

정리 #

이번 글에서 잡은 것:

  • securityContext의 두 층위. Pod 레벨은 공통 기본값, 컨테이너 레벨은 예외이며 컨테이너 레벨이 우선
  • 실행 사용자 제어. runAsUser,runAsGroup,runAsNonRoot로 비루트 강제, fsGroup으로 볼륨 쓰기 권한 확보
  • 파일시스템 통제. readOnlyRootFilesystem + emptyDir 우회, allowPrivilegeEscalation: false
  • capabilities. drop: ["ALL"] 후 최소 add, privileged: true는 격리를 무너뜨리므로 회피
  • 검증. k exec -- id,whoami로 실제 신원 확인

다음: 리소스 관리 #

컨테이너의 권한을 좁혔으니, 이제 컨테이너가 얼마나 많은 자원을 쓸 수 있는지를 통제할 차례입니다.

#16 리소스 관리: requests/limits, QoS class, LimitRange에서는 CPU,메모리의 requestslimits가 스케줄링과 OOM에 미치는 영향, 그 조합으로 결정되는 QoS class(Guaranteed,Burstable,BestEffort), 네임스페이스 단위 기본값을 거는 LimitRange와 ResourceQuota까지 직접 만들어 보며 정리하겠습니다.

X