Certified Kubernetes Security Specialist (CKS) #9 Pod Security Admission (PSA, Pod Security Standards)

#8 kernel hardening, capabilities, /proc 보호까지는 노드와 컨테이너를 리눅스 커널 수준에서 가두는 작업을 다뤘습니다. 이번 글부터는 Minimize Microservice Vulnerabilities도메인으로 넘어와, 위험한 설정을 가진 Pod 자체를 클러스터가 받아들이지 않게 만드는 방법을 봅니다. 그 첫 도구가 쿠버네티스에 내장된 **Pod Security Admission(PSA)**입니다.

PSA는 별도 설치 없이 API 서버에 내장된 admission controller입니다. 네임스페이스에 label 하나만 붙이면, 그 네임스페이스에 들어오는 모든 Pod가 정해진 보안 기준을 만족하는지 검사받고, 위반하면 생성 자체가 거부됩니다. 시험에서는 “이 네임스페이스에 restricted 기준을 enforce로 적용하라”, “거부당하는 Pod를 기준에 맞게 고쳐라” 유형이 단골로 나옵니다.

PodSecurityPolicy는 폐지되었다 #

먼저 배경을 한 줄로 정리하겠습니다. 과거에는 **PodSecurityPolicy(PSP)**라는 admission controller가 같은 역할을 했지만, 권한 부여 모델이 복잡하고 사용하기 어렵다는 이유로 v1.21에서 deprecated, v1.25에서 완전히 제거되었습니다. 그 대체재가 v1.25부터 stable이 된 Pod Security Admission입니다. 현재 CKS 시험 버전에서는 PSP가 존재하지 않으므로, Pod 단위 보안 기준은 모두 PSA로 다룬다고 보면 됩니다.

PSA는 PSP와 달리 별도의 정책 리소스를 만들거나 RBAC을 엮을 필요가 없습니다. 미리 정의된 Pod Security Standards라는 세 단계 기준을, 네임스페이스 label로 골라 적용하는 것이 전부입니다.

Pod Security Standards 세 단계 #

Pod Security Standards는 보안 강도를 세 단계로 나눈 표준 프로파일입니다. 단계가 올라갈수록 Pod에 허용되는 위험한 설정이 줄어듭니다.

단계의미허용 범위
privileged제한 없음모든 설정 허용. host 네임스페이스,privileged 컨테이너,임의 capabilities까지 전부 가능
baseline알려진 권한 상승을 막는 최소선privileged 컨테이너,hostNetwork,hostPID,위험한 capabilities 등을 금지. 그 외 기본 설정은 대체로 허용
restricted강하게 굳힌 모범 사례baseline의 금지에 더해 runAsNonRoot,seccomp RuntimeDefault,capabilities drop ALL 등을 적극 요구

세 단계의 차이를 한 줄로 요약하면 이렇습니다. privileged는 아무것도 막지 않고, baseline은 명백한 탈출 경로만 막고, restricted는 최소 권한을 강제합니다. privileged는 보통 시스템 컴포넌트나 노드 에이전트처럼 호스트 접근이 꼭 필요한 워크로드에만 쓰고, 일반 애플리케이션 네임스페이스에는 baseline 또는 restricted를 적용하는 것이 권장 방향입니다.

baseline이 막는 것 #

baseline은 컨테이너가 노드를 장악하는 알려진 경로를 차단합니다. 주요 금지 항목은 다음과 같습니다.

  • hostNetwork, hostPID, hostIPC 등 host 네임스페이스 공유
  • privileged: true 컨테이너
  • hostPath 볼륨(일부 예외 제외)
  • NET_RAW를 제외한 위험한 추가 capabilities
  • 기본 seccomp 프로파일을 Unconfined로 명시하는 것

restricted가 추가로 요구하는 것 #

restricted는 baseline의 금지를 모두 포함하면서, 거기에 더해 다음을 반드시 설정하도록 요구합니다. 시험에서 restricted를 통과하는 Pod를 작성하려면 이 목록이 곧 체크리스트입니다.

  • runAsNonRoot: true (root로 실행 금지)
  • allowPrivilegeEscalation: false
  • seccompProfile.type: RuntimeDefault(또는 Localhost)
  • capabilities.dropALL 포함. 추가로 허용되는 것은 NET_BIND_SERVICE
  • hostPath 같은 볼륨 타입을 제외한 안전한 볼륨만 사용

이 설정은 Pod 레벨의 securityContext와 컨테이너 레벨의 securityContext 양쪽에 적절히 들어가야 합니다. 특히 runAsNonRootseccompProfile은 Pod 레벨에, allowPrivilegeEscalationcapabilities는 컨테이너 레벨에 두는 것이 일반적입니다.

PSA 모드 세 종류 #

같은 기준이라도 위반했을 때 무엇을 하는가를 모드로 고릅니다. PSA는 세 모드를 제공하며, 한 네임스페이스에 동시에 서로 다른 단계로 걸 수도 있습니다.

모드동작용도
enforce기준을 위반한 Pod의 생성을 거부. Pod가 만들어지지 않음실제 차단
audit위반해도 생성은 허용하되, audit log에 위반 사실을 기록영향 분석
warn위반해도 생성은 허용하되, kubectl 응답에 경고 메시지를 표시사용자 안내

운영에서는 보통 새 기준을 곧장 enforce로 걸기 전에 warn과 audit으로 먼저 영향을 살핀 뒤, 안전을 확인하고 enforce로 올립니다. 다만 시험에서는 대개 enforce 하나만 명확히 요구하므로, 문제가 “enforce로 restricted를 적용하라"라고 하면 enforce label만 정확히 붙이면 됩니다.

네임스페이스 label로 적용한다 #

PSA의 적용 단위는 네임스페이스입니다. 네임스페이스에 다음 형식의 label을 붙이면 그 네임스페이스의 모든 Pod에 기준이 적용됩니다.

pod-security.kubernetes.io/<mode>: <level>
pod-security.kubernetes.io/<mode>-version: <version>
  • <mode>enforce, audit, warn 중 하나입니다.
  • <level>privileged, baseline, restricted 중 하나입니다.
  • -version label은 적용할 기준의 쿠버네티스 버전을 고정합니다. 생략하면 latest로 동작하며, 명시할 때는 v1.30처럼 적습니다.

예를 들어 app 네임스페이스에 restricted를 enforce로 거는 label은 다음과 같습니다.

apiVersion: v1
kind: Namespace
metadata:
  name: app
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest

위처럼 enforce와 warn을 함께 걸면, 위반 Pod는 거부되면서 동시에 사람이 읽기 쉬운 경고도 표시됩니다. 시험에서 빠르게 처리하려면 매니페스트 대신 kubectl label로 거는 방법도 익혀 두면 좋습니다.

# 이미 존재하는 네임스페이스에 enforce restricted 적용
kubectl label namespace app \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest --overwrite

--overwrite를 붙여야 이미 다른 값이 있는 경우에도 덮어쓸 수 있습니다.

위반 Pod는 어떻게 거부되는가 #

enforce 모드가 걸린 네임스페이스에 기준을 위반한 Pod를 만들려고 하면, API 서버가 admission 단계에서 막고 오류 메시지를 돌려줍니다. 예를 들어 restricted가 enforce된 네임스페이스에 아무 설정 없는 평범한 nginx Pod를 만들면 다음과 비슷한 거부 메시지가 나옵니다.

Error from server (Forbidden): error when creating "pod.yaml": pods "nginx" is forbidden:
violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (...),
unrestricted capabilities (...), runAsNonRoot != true (...),
seccompProfile (...) must be set to "RuntimeDefault" or "Localhost"

이 메시지가 곧 수정 가이드입니다. 위반 항목이 그대로 나열되므로, 메시지에 적힌 필드를 하나씩 채워 넣으면 통과합니다. 시험에서 “거부당하는 Pod를 고쳐라"가 나오면, 먼저 그대로 한번 적용해 거부 메시지를 받아 보고, 나열된 항목을 securityContext에 반영하는 흐름이 빠릅니다.

이미 돌고 있던 Deployment의 Pod에는 영향이 없습니다. PSA는 admission 단계에서만 동작하므로, label을 건 뒤 새로 생성되는 Pod부터 검사를 받습니다. 따라서 기존 워크로드에 restricted를 적용했다면, Deployment를 재배포해 새 Pod를 만들어 봐야 실제 위반 여부가 드러납니다.

restricted를 통과하는 Pod 예제 #

restricted가 enforce된 네임스페이스에서도 정상적으로 생성되는 Pod는 다음과 같이 작성합니다. 위의 체크리스트가 모두 반영되어 있습니다.

apiVersion: v1
kind: Pod
metadata:
  name: secure-nginx
  namespace: app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: nginx
      image: nginxinc/nginx-unprivileged:stable
      ports:
        - containerPort: 8080
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop:
            - ALL

핵심을 다시 짚겠습니다.

  • Pod 레벨 securityContextrunAsNonRoot: trueseccompProfile.type: RuntimeDefault를 둡니다.
  • 컨테이너 레벨 securityContextallowPrivilegeEscalation: falsecapabilities.drop: [ALL]을 둡니다.
  • 이미지는 root가 아닌 사용자로 도는 것을 골라야 합니다. 일반 nginx이미지는 80번 포트를 root로 바인딩하므로 runAsNonRoot와 충돌합니다. 위 예제처럼 비특권 이미지를 쓰거나 포트를 1024 이상으로 바꿉니다.

마지막 항목이 실수하기 쉬운 부분입니다. securityContext 필드만 채우고 root로 도는 이미지를 그대로 쓰면, 생성은 통과해도 컨테이너가 기동 단계에서 실패합니다. 기준을 만족하는 매니페스트와 실제로 비특권으로 도는 이미지가 둘 다 맞아야 합니다.

클러스터 전체 기본값(AdmissionConfiguration) #

네임스페이스마다 label을 거는 대신, 클러스터 전체에 기본 기준을 깔 수도 있습니다. API 서버에 AdmissionConfiguration 파일을 넘겨 PodSecurity 플러그인의 기본값과 예외를 정합니다. 시스템 네임스페이스(kube-system 등)는 보통 예외로 빼 둡니다.

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
  - name: PodSecurity
    configuration:
      apiVersion: pod-security.admission.config.k8s.io/v1
      kind: PodSecurityConfiguration
      defaults:
        enforce: baseline
        enforce-version: latest
      exemptions:
        namespaces:
          - kube-system

이 파일은 kube-apiserver의 --admission-control-config-file 플래그로 연결합니다. 다만 시험에서 흔한 형태는 클러스터 전역 설정보다 개별 네임스페이스 label이므로, 우선순위는 label 방식에 두는 것이 안전합니다.

시험 포인트 #

  • PSP는 v1.25에서 제거되었고 그 대체가 PSA입니다. PSP 문법을 쓰면 안 됩니다.
  • 세 단계는 privileged → baseline → restricted 순으로 강해집니다. restricted가 가장 엄격합니다.
  • 세 모드는 **enforce(거부),audit(기록),warn(경고)**입니다. 실제 차단은 enforce뿐입니다.
  • 적용 단위는 네임스페이스이며, label 키는 pod-security.kubernetes.io/<mode>pod-security.kubernetes.io/<mode>-version입니다.
  • restricted 통과 체크리스트는 runAsNonRoot: true, allowPrivilegeEscalation: false, seccompProfile.type: RuntimeDefault, capabilities.drop: [ALL]입니다.
  • 거부 메시지에 위반 항목이 그대로 나오므로, 먼저 적용해 메시지를 받고 그대로 고치는 흐름이 빠릅니다.
  • root로 도는 이미지는 securityContext만 채워도 기동에 실패합니다. 비특권 이미지까지 함께 챙겨야 합니다.

PSA의 보안 강화 방향은 CKAD #15 securityContext와 Pod 보안에서 다룬 Pod 레벨 보안 설정과 같은 필드를 쓰며, PSA는 그 설정을 네임스페이스 단위로 강제한다는 점만 다릅니다. CKAD가 권장 설정을 손에 익히는 단계였다면, CKS는 그 설정을 클러스터가 검사하도록 만드는 단계입니다.

정리 #

이번 글에서 잡은 것:

  • PodSecurityPolicy 폐지 후 그 역할을 Pod Security Admission이 이어받았습니다. 별도 설치 없이 API 서버에 내장되어 있습니다.
  • Pod Security Standards 세 단계(privileged,baseline,restricted)는 단계가 올라갈수록 허용 범위가 좁아집니다.
  • 세 모드(enforce,audit,warn)로 위반 시 동작을 고르며, 실제 거부는 enforce입니다.
  • 네임스페이스 label pod-security.kubernetes.io/enforce: restricted와 enforce-version으로 적용합니다.
  • restricted 통과 Pod는 runAsNonRoot,allowPrivilegeEscalation: false,seccomp RuntimeDefault,capabilities drop ALL을 갖추고, 비특권 이미지로 돌아야 합니다.

다음: Secrets 관리 #

Pod의 보안 기준은 PSA로 강제할 수 있게 되었습니다. 그런데 Pod가 다루는 민감한 데이터, 곧 Secret 자체는 어떻게 보호할까요. 기본 Secret는 etcd에 평문에 가까운 base64로 저장되어, etcd에 접근할 수 있는 사람은 그대로 읽을 수 있습니다.

#10 Secrets 관리: etcd 암호화, External Secrets에서는 EncryptionConfiguration으로 etcd에 저장되는 Secret를 암호화하는 법, 키 회전, 그리고 외부 비밀 저장소를 연동하는 External Secrets 패턴까지 시험 관점에서 정리하겠습니다.

X