Pod Security — 컨테이너 실행 권한을 제한하는 방법
컨테이너가 root 권한으로 실행되고, 호스트 네트워크에 접근할 수 있다면, 컨테이너 격리의 의미가 있을까요?
컨테이너는 기본적으로 호스트와 격리되지만, 설정에 따라 격리가 무너질 수 있습니다. privileged 모드로 실행되거나, root 사용자로 동작하거나, 호스트의 네임스페이스에 접근하면 컨테이너 탈출(container escape)의 위험이 커집니다. Pod Security는 이런 위험한 설정을 제한하는 방어 체계입니다.
securityContext — 컨테이너 보안 설정
securityContext는 Pod이나 컨테이너 수준에서 보안 관련 설정을 제어합니다.
컨테이너 수준
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 수준
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000 # 볼륨의 파일 그룹 소유권
seccompProfile:
type: RuntimeDefault # seccomp 프로파일 적용
주요 설정 항목
| 설정 | 효과 | 권장 |
|---|---|---|
runAsNonRoot: true | root 실행 차단 | 필수 |
readOnlyRootFilesystem: true | 쓰기 차단 | 권장 |
allowPrivilegeEscalation: false | setuid 등 차단 | 필수 |
capabilities.drop: [ALL] | 모든 리눅스 기능 제거 | 권장 |
privileged: false | 특권 모드 차단 | 필수 |
# 보안 강화된 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"]
readOnlyRootFilesystem을 설정하면 쓰기가 필요한 디렉토리는 emptyDir로 별도 마운트해야 합니다.
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 적용 방법
# 네임스페이스에 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 | 위반 시 사용자에게 경고 표시 (생성은 허용) |
점진적 도입 시 warn → audit → enforce 순서로 적용하는 것이 안전합니다.
# 네임스페이스에 직접 설정
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: truehostNetwork: truehostPID: true,hostIPC: true- 위험한 capabilities (SYS_ADMIN 등)
/proc마운트 변경
Restricted에서 추가로 요구하는 항목
runAsNonRoot: trueallowPrivilegeEscalation: falseseccompProfile설정- capabilities
drop: [ALL] runAsUser(UID 0 차단)
OPA Gatekeeper — 커스텀 정책
PSA로 커버되지 않는 세밀한 정책은 OPA(Open Policy Agent) Gatekeeper로 구현합니다.
# Gatekeeper 설치
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.15.0/deploy/gatekeeper.yaml
정책 예제: 허용된 이미지 레지스트리만 사용
# ConstraintTemplate — 정책 템플릿 정의
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
type: object
Pod 템플릿과 컨테이너 스펙을 이어서 정의합니다.
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])
}
ConstraintTemplate을 정의한 후, Constraint 오브젝트로 어떤 네임스페이스의 어떤 리소스에 적용할지 지정합니다.
---
# 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도 있습니다.
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: "?*"
보안 강화 체크리스트
# 프로덕션 워크로드 보안 체크리스트
spec:
serviceAccountName: dedicated-sa # 전용 ServiceAccount
automountServiceAccountToken: false # 불필요한 API 토큰 마운트 차단
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
컨테이너 수준에서는 권한 상승 차단, 읽기 전용 파일시스템, capability 제거, 리소스 제한까지 모두 설정합니다.
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
주의할 점
1. restricted 정책을 갑자기 enforce로 적용하면 기존 워크로드가 전부 거부된다
PSA(Pod Security Admission)의 enforce 모드는 정책을 위반하는 Pod 생성을 즉시 차단합니다. 기존에 root로 실행되던 워크로드가 있는 네임스페이스에 바로 적용하면 새 Pod이 뜨지 않아 서비스 장애가 발생합니다. 먼저 warn이나 audit 모드로 위반 사항을 확인한 뒤, 워크로드를 수정하고 나서 enforce로 전환하세요.
2. readOnlyRootFilesystem을 설정하면 로그 파일이나 임시 파일 쓰기가 실패한다
보안을 위해 루트 파일시스템을 읽기 전용으로 설정하면, 애플리케이션이 /tmp이나 /var/log에 파일을 쓰려다 Permission denied로 죽습니다. 쓰기가 필요한 경로에는 emptyDir 볼륨을 마운트하거나, tmpfs를 설정해서 필요한 쓰기 공간을 확보해야 합니다.
3. capabilities를 ALL drop했는데 NET_BIND_SERVICE를 빠뜨리면 80 포트 바인딩이 실패한다
1024 미만의 포트에 바인딩하려면 NET_BIND_SERVICE capability가 필요합니다. drop: ["ALL"]만 하고 필요한 capability를 add하지 않으면 Nginx나 Apache 같은 웹 서버가 시작에 실패합니다. 최소 권한 원칙은 "전부 제거 후 필요한 것만 추가"입니다.
정리
Pod Security는 securityContext로 개별 컨테이너의 권한을 제한하고, PSA로 네임스페이스 수준의 정책을 적용하며, OPA Gatekeeper/Kyverno로 커스텀 정책을 구현하는 다층 방어 체계입니다. runAsNonRoot, readOnlyRootFilesystem, capabilities drop ALL은 프로덕션 워크로드의 기본 설정으로 생각하는 것이 좋습니다.