CKS — 쿠버네티스 보안 전문가 · 시스템 하드닝 · 실습
seccomp·AppArmor·capability 로 조이기
목표
파드 스펙만으로 컨테이너가 커널에 요청할 수 있는 일을 줄이는 방법을 손에 익히고,
클러스터에 남아 있는 privileged 컨테이너를 찾아내는 탐지 절차를 만듭니다.
왜 중요한가
컨테이너는 호스트 커널을 공유합니다. 탈출 취약점은 대부분 시스템 콜이나 capability 를 통해
커널을 찌릅니다. 그래서 방어는 침입을 막는 쪽이 아니라, 침입한 프로세스가 할 수 있는 일을
미리 줄여 두는 쪽으로 설계합니다. seccomp 는 호출 가능한 시스템 콜을, AppArmor 는 접근 가능한
파일과 기능을, capabilities 는 root 권한의 조각을 각각 제한합니다. 세 층이 겹치기 때문에
하나가 뚫려도 나머지가 남습니다. 반대로 privileged: true 는 이 세 층을 한 번에 무효화하므로,
운영 클러스터에서 privileged 컨테이너를 세는 일이 곧 위험도 측정입니다.
주의: 이 실습 환경에는 실제 컨테이너 런타임이 없습니다. seccomp·AppArmor 프로파일이 정말로
강제되는지는 확인할 수 없고, 노드에 프로파일 파일이 있는지도 검사하지 않습니다. 채점은 파드
스펙의 필드가 정확히 쓰였는지만 봅니다. CKS 시험에서 채점하는 것도 결국 이 스펙입니다.
단계
1. 네임스페이스 cks-sys 를 만들고 그 안에 파드 seccomp-default(이미지nginx:1.27-alpine)를 만든다. 파드 레벨 spec.securityContext.seccompProfile.type 은RuntimeDefault 다.
2. cks-sys 에 파드 seccomp-custom 을 만든다. 파드 레벨 seccompProfile 의 type 은Localhost, localhostProfile 은 profiles/audit.json 이다.
3. cks-sys 에 파드 apparmor-app(컨테이너 이름 app, 이미지 nginx:1.27-alpine)을 만들고
AppArmor 프로파일 k8s-custom-profile 을 건다. 컨테이너securityContext.appArmorProfile 의 type 은 Localhost, localhostProfile 은k8s-custom-profile 이다. (구버전 형식인container.apparmor.security.beta.kubernetes.io/app: localhost/k8s-custom-profile
어노테이션으로 해도 통과한다.)
4. cks-sys 에 파드 capped(컨테이너 이름 app)를 만든다. 컨테이너 securityContext 에서capabilities.drop 은 ALL 하나, capabilities.add 는 NET_BIND_SERVICE 하나이고allowPrivilegeEscalation 은 false 다.
5. cks-sys 에 파드 no-host-ns 를 만든다. hostNetwork, hostPID, hostIPC,shareProcessNamespace 를 모두 false 로 명시한다.
여기에 감사에서 반드시 알아야 할 사실이 하나 숨어 있다. PodSpec 에서 앞의 세 필드는bool + omitempty 라, false 로 적어도 직렬화 단계에서 통째로 빠진다. 즉 저장된
오브젝트만 보면 "false 라고 적었다" 와 "아예 안 적었다" 를 구별할 수 없다. 그래서
실제 감사 기준은 "false 라고 적혀 있나" 가 아니라 "true 가 아닌가" 다. 반면shareProcessNamespace 는 *bool 이라 false 가 그대로 남는다 — 채점기도
이 차이를 그대로 반영한다. 적용한 뒤 kubectl get pod no-host-ns -n cks-sys -o yaml
로 직접 확인해 보라.
6. cks-sys 에 파드 legacy-agent(컨테이너 이름 agent)를 만들되 컨테이너
securityContext 의 privileged 를 true 로 둔다(레거시 워크로드 재현).
그다음 /root/cks-system-hardening/find-privileged.sh 에 탐지 스크립트를 쓰고,
그 실행 결과를 /root/cks-system-hardening/privileged.txt 에 저장한다. 결과는 cks-sys
네임스페이스에서 privileged 컨테이너(초기화 컨테이너 포함)를 가진 파드를cks-sys/<파드이름> 형식으로 한 줄에 하나씩, 사전순 정렬한 것이다.
7. cks-sys 에 파드 hardened(컨테이너 이름 app)를 만든다. 컨테이너 securityContext 에readOnlyRootFilesystem: true, runAsNonRoot: true, runAsUser: 1000,allowPrivilegeEscalation: false, capabilities.drop: [ALL] 을 모두 넣고,
파드 레벨 seccompProfile.type 은 RuntimeDefault 로 둔다. 쓰기가 필요한 /tmp 는
이름이 tmp 인 emptyDir 볼륨으로 마운트한다.
참고
kubectl run seccomp-default --image=nginx:1.27-alpine -n cks-sys --dry-run=client -o yaml > pod.yaml로 초안을 뽑고 편집하는 편이 빠릅니다.kubectl explain pod.spec.securityContext.seccompProfile로 필드 이름을 확인하세요.- 탐지 스크립트 힌트:
kubectl get pods -n cks-sys -o json | jq -r '...'에서.spec.containers[]와.spec.initContainers[]를 함께 훑습니다. - 흔한 실수 1:
capabilities를 파드 레벨 securityContext 에 넣습니다. capabilities 는 컨테이너 레벨에만 있습니다. - 흔한 실수 2:
localhostProfile에 절대 경로를 씁니다. 노드의 seccomp 루트 기준 상대 경로여야 합니다.
단계 7개
- 런타임 기본 seccomp 프로파일
- 커스텀 seccomp 프로파일 지정
- AppArmor 프로파일 붙이기
- capability 전부 버리고 하나만 되찾기
- 호스트 네임스페이스 차단
- privileged 컨테이너 탐지
- 읽기 전용 루트 파일시스템 종합