Theme:

컨테이너가 root 권한으로 실행되고, 호스트 네트워크에 접근할 수 있다면, 컨테이너 격리의 의미가 있을까요?

컨테이너는 기본적으로 호스트와 격리되지만, 설정에 따라 격리가 무너질 수 있습니다. privileged 모드로 실행되거나, root 사용자로 동작하거나, 호스트의 네임스페이스에 접근하면 컨테이너 탈출(container escape)의 위험이 커집니다. Pod Security는 이런 위험한 설정을 제한하는 방어 체계입니다.

securityContext — 컨테이너 보안 설정

securityContext는 Pod이나 컨테이너 수준에서 보안 관련 설정을 제어합니다.

컨테이너 수준

YAML
spec:
  containers:
    - name: app
      image: my-app:1.0
      securityContext:
        runAsNonRoot: true                # root 실행 거부
        runAsUser: 1000                   # UID 1000으로 실행
        runAsGroup: 1000                  # GID 1000으로 실행
        readOnlyRootFilesystem: true      # 루트 파일시스템 읽기 전용
        allowPrivilegeEscalation: false   # 권한 상승 차단
        capabilities:
          drop:
            - ALL                          # 모든 리눅스 capability 제거
          add:
            - NET_BIND_SERVICE             # 필요한 것만 추가

Pod 수준

YAML
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000               # 볼륨의 파일 그룹 소유권
    seccompProfile:
      type: RuntimeDefault      # seccomp 프로파일 적용

주요 설정 항목

설정효과권장
runAsNonRoot: trueroot 실행 차단필수
readOnlyRootFilesystem: true쓰기 차단권장
allowPrivilegeEscalation: falsesetuid 등 차단필수
capabilities.drop: [ALL]모든 리눅스 기능 제거권장
privileged: false특권 모드 차단필수
YAML
# 보안 강화된 Pod 예제
apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: my-app:1.0
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      volumeMounts:
        - name: tmp
          mountPath: /tmp       # 쓰기 필요한 경로만 emptyDir로
        - name: cache
          mountPath: /app/cache
  volumes:
    - name: tmp
      emptyDir: {}
    - name: cache
      emptyDir: {}

readOnlyRootFilesystem을 설정하면 애플리케이션이 파일을 쓸 수 없으므로, 쓰기가 필요한 디렉토리는 emptyDir로 마운트합니다.

Pod Security Admission (PSA)

PSA는 Kubernetes 1.25에서 정식(GA) 기능이 된 내장 보안 정책입니다. 네임스페이스에 레이블을 추가하여 세 가지 프로파일을 적용합니다.

세 가지 프로파일

프로파일제한 수준허용 범위
Privileged제한 없음모든 설정 허용
Baseline최소 제한알려진 위험 설정만 차단 (privileged, hostNetwork 등)
Restricted최대 제한비root, 읽기 전용 FS, capability 제거 등 요구

PSA 적용 방법

SHELL
# 네임스페이스에 PSA 레이블 추가
kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/warn=restricted

세 가지 동작 모드

모드동작
enforce위반 시 Pod 생성 거부
audit위반 시 감사 로그 기록 (생성은 허용)
warn위반 시 사용자에게 경고 표시 (생성은 허용)

점진적 도입 시 warnauditenforce 순서로 적용하는 것이 안전합니다.

YAML
# 네임스페이스에 직접 설정
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

Baseline에서 차단되는 항목

  • privileged: true
  • hostNetwork: true
  • hostPID: true, hostIPC: true
  • 위험한 capabilities (SYS_ADMIN 등)
  • /proc 마운트 변경

Restricted에서 추가로 요구하는 항목

  • runAsNonRoot: true
  • allowPrivilegeEscalation: false
  • seccompProfile 설정
  • capabilities drop: [ALL]
  • runAsUser (UID 0 차단)

OPA Gatekeeper — 커스텀 정책

PSA로 커버되지 않는 세밀한 정책은 OPA(Open Policy Agent) Gatekeeper로 구현합니다.

SHELL
# Gatekeeper 설치
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.15.0/deploy/gatekeeper.yaml

정책 예제: 허용된 이미지 레지스트리만 사용

YAML
# ConstraintTemplate — 정책 템플릿 정의
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not startswith(container.image, input.parameters.repos[_])
          msg := sprintf("이미지 '%v'는 허용된 레지스트리가 아닙니다", [container.image])
        }
---
# Constraint — 정책 적용
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: allowed-repos
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces: ["production"]
  parameters:
    repos:
      - "myregistry.io/"
      - "docker.io/library/"

다른 유용한 Gatekeeper 정책

  • 특정 레이블 필수 (모든 리소스에 team 레이블 요구)
  • 리소스 제한 필수 (requests/limits 없는 Pod 차단)
  • 최신 태그 차단 (latest 태그 사용 금지)
  • 외부 로드밸런서 제한

Kyverno — YAML 기반 정책

Gatekeeper의 대안으로, Rego 대신 YAML로 정책을 작성할 수 있는 Kyverno도 있습니다.

YAML
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-team-label
      match:
        any:
          - resources:
              kinds: ["Deployment"]
      validate:
        message: "team 레이블이 필요합니다"
        pattern:
          metadata:
            labels:
              team: "?*"

보안 강화 체크리스트

YAML
# 프로덕션 워크로드 보안 체크리스트
spec:
  serviceAccountName: dedicated-sa         # 전용 ServiceAccount
  automountServiceAccountToken: false      # 불필요한 API 토큰 마운트 차단
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: myregistry.io/app:v1.2.3     # latest 태그 사용 금지
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      resources:
        requests:
          cpu: 200m
          memory: 256Mi
        limits:
          memory: 512Mi

정리

Pod Security는 securityContext로 개별 컨테이너의 권한을 제한하고, PSA로 네임스페이스 수준의 정책을 적용하며, OPA Gatekeeper/Kyverno로 커스텀 정책을 구현하는 다층 방어 체계입니다. runAsNonRoot, readOnlyRootFilesystem, capabilities drop ALL은 프로덕션 워크로드의 기본 설정으로 생각하는 것이 좋습니다.

댓글 로딩 중...