컨테이너가 기본값으로 root(UID 0)로 돌고 파일시스템 전체에 쓰기가 가능하다면, 컨테이너가 뚫렸을 때 공격자가 그대로 root 권한을 얻습니다. SecurityContext는 "이 컨테이너는 root가 아니어야 하고, 디스크에 함부로 못 쓴다"를 쿠버네티스 수준에서 강제하는 장치입니다. 핵심은 두 가지, runAsNonRoot와 readOnlyRootFilesystem입니다.
지금 root로 돌고 있는지 확인
# 컨테이너 안에서 실행 중인 UID 확인
kubectl exec <pod> -- id
# uid=0(root) 이면 위험 신호
uid=0(root)가 나오면 이미지가 root로 돌고 있다는 뜻입니다.
적용할 설정
apiVersion: v1
kind: Pod
metadata:
name: hardened
spec:
securityContext:
runAsNonRoot: true # root면 아예 기동 거부
runAsUser: 1000 # 비-root UID 지정
fsGroup: 2000 # 볼륨 그룹 소유권
containers:
- name: app
image: myapp:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true # 루트 FS 쓰기 금지
capabilities:
drop: ["ALL"] # 모든 리눅스 capability 제거
옵션별 의미
| 옵션 | 역할 | 효과 |
|---|---|---|
runAsNonRoot: true | root 기동 차단 | UID 0이면 Pod이 시작 실패 |
runAsUser: 1000 | 실행 UID 강제 | 이미지가 root여도 1000으로 |
readOnlyRootFilesystem | 루트 FS 읽기전용 | 침투해도 바이너리 변조 불가 |
allowPrivilegeEscalation: false | setuid 권한 상승 차단 | sudo류 권한 획득 봉쇄 |
capabilities.drop: ALL | 커널 권한 최소화 | 필요한 것만 add로 다시 부여 |
적용하면 깨지는 흔한 케이스
① runAsNonRoot인데 이미지가 root — Pod이 CreateContainerConfigError로 뜨고 container has runAsNonRoot and image will run as root 메시지가 납니다. Dockerfile에 USER 1000을 넣어 비-root 이미지로 다시 빌드해야 합니다.
② readOnlyRootFilesystem인데 앱이 임시파일을 씀 — /tmp나 캐시 디렉터리에 쓰려다 read-only file system 에러가 납니다. 쓰기가 필요한 경로만 emptyDir 볼륨으로 마운트해 뚫어줍니다.
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
③ 포트 바인딩 실패 — 비-root는 1024 미만 포트를 못 엽니다. 80 대신 8080처럼 높은 포트를 쓰고 Service에서 매핑합니다.
체크리스트
kubectl exec <pod> -- id # 비-root UID 확인
kubectl get pod <pod> -o jsonpath='{.spec.securityContext}'
kubectl describe pod <pod> # 기동 실패 사유 확인
세 줄이면 root 잔존·읽기전용 충돌을 바로 잡아냅니다.
SecurityContext를 직접 걸고 일부러 깨뜨려 보며 어떤 에러가 나는지 관찰하는 실습은 쿠버네티스 트랙에서 해볼 수 있습니다 — 회원가입 없이 무료로.