근묵자흑
Kubernetes Pattern: Process Containment 본문
컨테이너 프로세스 격리와 최소 권한 원칙
Kubernetes 환경에서는 정적 코드 분석, 취약점 스캐닝, 이미지 검사를 수행하더라도 런타임에 새로운 취약점이 발견되거나 제로데이 공격이 발생할 수 있습니다. Process Containment 패턴은 런타임 프로세스 수준의 보안 제어를 통해 "컨테이너 내부에서 발생한 침해가 컨테이너 외부로 확산되지 않도록" 하는 방어 메커니즘입니다.
이 패턴의 핵심 원칙은 최소 권한(Principle of Least Privilege)입니다. 프로세스가 작업 수행에 필요한 최소한의 권한만 보유하도록 제한하여, 보안 침해 발생 시 공격자가 수행할 수 있는 작업을 제한합니다. 컨테이너는 단순한 패키징 포맷이나 리소스 격리 메커니즘이 아니라, 적절히 설정되면 보안 울타리(security fence) 역할을 합니다.
flowchart TB
subgraph "보안 위협과 방어선"
A[애플리케이션 코드] --> B{보안 침해 발생}
B -->|침해 발생| C[런타임 보안 제어]
B -->|정상| F[정상 운영]
C --> D[SecurityContext]
C --> E[Capabilities]
C --> G[Seccomp]
D --> H{컨테이너 탈출 시도}
E --> H
G --> H
H -->|차단| I[격리 유지]
H -->|허용| J[호스트 침해]
end
style I fill:#90EE90
style J fill:#FFB6C1
문제 상황
Kubernetes 워크로드에 대한 주요 공격 경로는 애플리케이션 코드의 취약점을 통한 것입니다. 다음과 같은 보안 도구를 사용하더라도 위험을 완전히 제거할 수는 없습니다.
정적 코드 분석 도구는 소스 코드에서 보안 결함을 검사합니다. 동적 스캐닝 도구는 SQL 인젝션(SQLi), CSRF, XSS와 같은 잘 알려진 서비스 공격을 통해 시스템에 침입하려는 악의적인 공격자를 시뮬레이션합니다. 애플리케이션의 의존성을 정기적으로 스캔하여 보안 취약점을 확인하는 도구도 있습니다. 이미지 빌드 과정에서 컨테이너는 알려진 취약점에 대해 스캔됩니다.
문제는 새로운 코드와 의존성이 지속적으로 추가되면서 새로운 취약점이 도입되고, 제로데이 취약점은 스캐닝으로 탐지되지 않는다는 점입니다. 런타임 프로세스 수준의 보안 제어가 없다면 공격자는 다음과 같은 경로로 시스템을 침해할 수 있습니다.
flowchart LR
A[취약한 애플리케이션] --> B[코드 실행 취득]
B --> C[컨테이너 권한 악용]
C --> D[컨테이너 탈출]
D --> E[호스트 노드 접근]
E --> F[클러스터 전체 침해]
style A fill:#FFE4E1
style F fill:#FF6B6B
솔루션
Kubernetes에서 컨테이너에 적용될 보안 구성은 Pod와 컨테이너 스펙의 securityContext 설정을 통해 제어됩니다. Pod 수준 설정은 Pod의 볼륨과 모든 컨테이너에 적용되며, 컨테이너 수준 설정은 개별 컨테이너에 적용됩니다. 동일한 설정이 두 수준에서 모두 지정된 경우, 컨테이너 스펙의 값이 우선합니다.
flowchart TB
subgraph "보안 제어 계층"
A[Pod/Container Spec] --> B[SecurityContext]
B --> C[User/Group 설정]
B --> D[Capabilities 제한]
B --> E[파일시스템 보호]
B --> F[Seccomp 프로파일]
A --> G[Pod Security Admission]
G --> H[Privileged Level]
G --> I[Baseline Level]
G --> J[Restricted Level]
end
style C fill:#E6F3FF
style D fill:#E6F3FF
style E fill:#E6F3FF
style F fill:#E6F3FF
1. Non-Root 사용자로 컨테이너 실행
컨테이너가 root 사용자로 실행되면 호스트의 root와 동일한 UID(0)를 가지며, 컨테이너 탈출 시 호스트에서도 root 권한을 얻을 수 있습니다. non-root 실행은 가장 기본적이면서도 효과적인 보안 제어입니다.
컨테이너 이미지에는 컨테이너 프로세스를 실행할 사용자와 선택적으로 그룹이 있습니다. 일부 컨테이너는 사용자가 생성되지 않아 기본적으로 root로 실행되고, 다른 컨테이너는 사용자가 생성되어 있지만 기본 사용자로 설정되지 않은 경우도 있습니다. 이러한 상황은 securityContext를 사용하여 런타임에 사용자를 재정의함으로써 해결할 수 있습니다.
명시적 UID/GID 설정
apiVersion: v1
kind: Pod
metadata:
name: web-app
spec:
securityContext:
runAsUser: 1000 # 컨테이너 프로세스를 실행할 UID
runAsGroup: 2000 # 컨테이너 프로세스를 실행할 GID
fsGroup: 3000 # 볼륨에 적용될 GID
containers:
- name: app
image: k8spatterns/random-generator:1.0
이 구성은 Pod 내 모든 컨테이너가 사용자 ID 1000과 그룹 ID 2000으로 실행되도록 강제합니다. 컨테이너 이미지에 지정된 사용자를 교체하고 싶을 때 유용합니다.
테스트 결과:
$ kubectl exec web-app -- id
uid=1000 gid=2000 groups=2000,3000
다만 주의할 점이 있습니다. 사용자 ID와 그룹 ID는 종종 컨테이너 이미지 내 디렉토리 구조의 파일 소유권과 함께 설정됩니다. 권한 부족으로 인한 런타임 오류를 피하려면 컨테이너 이미지 파일을 확인하고 정의된 사용자 ID와 그룹 ID로 컨테이너를 실행해야 합니다.
runAsNonRoot 강제
특정 사용자 ID를 지정하지 않고 컨테이너가 root로 실행되지 않도록 보장하는 덜 침습적인 방법은 runAsNonRoot 플래그를 true로 설정하는 것입니다. 이 설정 시 kubelet은 런타임에 검증을 수행하고 UID 0을 가진 root 사용자로 시작하는 모든 컨테이너를 차단합니다.
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true # root 사용자(UID 0) 실행 차단
containers:
- name: app
image: nginx
securityContext:
allowPrivilegeEscalation: false # 권한 상승 차단
테스트 결과: nginx 이미지는 기본적으로 root로 실행되므로 Pod 생성이 거부됩니다.
Error: container has runAsNonRoot and image will run as root
allowPrivilegeEscalation: false 설정은 컨테이너가 root가 아니더라도 권한 상승을 통해 root와 같은 권한을 얻는 것을 방지합니다. 이는 Linux의 sudo 명령어를 사용하여 root 권한으로 명령을 실행하는 것과 유사한 상황을 차단합니다.
컨테이너 내에서 파일이나 볼륨에 접근하기 위해 root로 실행해야 하는 경우, init 컨테이너를 사용하여 노출을 제한할 수 있습니다. init 컨테이너가 짧은 시간 동안 root로 실행되어 파일 접근 모드를 변경한 후, 애플리케이션 컨테이너는 non-root로 시작할 수 있습니다.
2. 컨테이너 Capabilities 제한
본질적으로 컨테이너는 노드에서 실행되는 프로세스이며, 프로세스가 가질 수 있는 것과 동일한 권한을 가질 수 있습니다. 프로세스가 커널 수준 호출을 필요로 하는 경우, 성공하려면 해당 권한이 있어야 합니다. 이를 위해 컨테이너를 root로 실행하여 모든 권한을 부여하거나, 애플리케이션이 기능하는 데 필요한 특정 capabilities만 할당할 수 있습니다.
flowchart LR
A[Root 권한] --> B[40+ Capabilities로 분리]
B --> C[NET_BIND_SERVICE]
B --> D[SYS_ADMIN]
B --> E[CHOWN]
B --> F[DAC_OVERRIDE]
B --> G[...]
H[컨테이너] --> I[필요한 것만 선택]
I --> C
privileged: true 플래그가 설정된 컨테이너는 본질적으로 호스트의 root와 동등하며 커널 권한 검사를 우회합니다. 보안 관점에서 이 옵션은 컨테이너를 격리하는 대신 호스트 시스템과 묶어버립니다. 따라서 이 플래그는 일반적으로 네트워크 스택을 조작하거나 하드웨어 장치에 접근하는 등 관리 기능을 가진 컨테이너에만 설정됩니다.
더 나은 접근 방식은 privileged 컨테이너 사용을 완전히 피하고 필요한 컨테이너에 특정 커널 capabilities를 부여하는 것입니다. Linux에서 전통적으로 root 사용자와 연관된 권한은 독립적으로 활성화하고 비활성화할 수 있는 개별 capabilities로 나뉩니다.
컨테이너 런타임은 컨테이너에 기본 권한(capabilities) 집합을 할당합니다. capabilities 섹션이 비어 있으면, 컨테이너 런타임이 정의한 기본 capabilities 집합은 대부분의 프로세스가 필요로 하는 것보다 훨씬 관대하여 익스플로잇에 노출될 수 있습니다. 컨테이너 공격 표면을 잠그는 좋은 보안 관행은 모든 권한을 제거하고 필요한 것만 추가하는 것입니다.
apiVersion: v1
kind: Pod
metadata:
name: web-app
spec:
containers:
- name: app
image: docker.io/centos/httpd
securityContext:
capabilities:
drop: ['ALL'] # 모든 기본 capabilities 제거
add: ['NET_BIND_SERVICE'] # 1024 이하 포트 바인딩 권한만 추가
이 예제에서는 모든 capabilities를 제거하고 NET_BIND_SERVICE capability만 다시 추가합니다. 이 capability는 1024보다 낮은 번호의 특권 포트에 바인딩할 수 있게 합니다. 대안적인 접근 방식은 컨테이너를 비특권 포트 번호에 바인딩하는 것으로 교체하는 것입니다.
테스트 결과:
# Capabilities 확인
$ kubectl exec web-server -- cat /proc/1/status | grep CapEff
CapEff: 0000000000000000 # drop ALL 시
$ kubectl exec web-server-with-netbind -- cat /proc/1/status | grep CapBnd
CapBnd: 0000000000000400 # NET_BIND_SERVICE만 활성화 (16진수 0x400)
포트 80에서 HTTP 서버를 실행할 수 있지만, 파일 소유권 변경(CHOWN)이나 시스템 관리 작업(SYS_ADMIN)은 불가능합니다.
주요 Linux Capabilities
| Capability | 용도 | 보안 위험 |
|---|---|---|
NET_BIND_SERVICE |
1024 이하 포트 바인딩 | 낮음 |
NET_RAW |
RAW 소켓 생성 (ping 등) | 중간 (패킷 스니핑 가능) |
SYS_ADMIN |
시스템 관리 작업 | 높음 (컨테이너 탈출 가능) |
SYS_PTRACE |
프로세스 디버깅 | 높음 (다른 프로세스 정보 획득) |
CHOWN |
파일 소유권 변경 | 중간 |
DAC_OVERRIDE |
파일 권한 무시 | 높음 (모든 파일 접근 가능) |
SETUID/SETGID |
프로세스 UID/GID 변경 | 높음 (권한 상승 가능) |
Pod의 Security Context가 설정되지 않거나 너무 허용적이면 침해 가능성이 높아집니다. 컨테이너의 capabilities를 최소한으로 제한하는 것은 알려진 공격에 대한 추가 방어선 역할을 합니다. 애플리케이션을 침해한 악의적인 행위자는 컨테이너 프로세스가 권한이 없거나 capabilities가 크게 제한되어 있을 때 호스트를 장악하기가 더 어려워집니다.
3. 변경 불가능한 컨테이너 파일시스템
일반적으로 컨테이너화된 애플리케이션은 컨테이너 파일시스템에 쓸 수 없어야 합니다. 컨테이너는 일시적이며 모든 상태는 재시작 시 손실되기 때문입니다. 상태는 데이터베이스나 파일시스템과 같은 외부 영속성 방법에 기록되어야 합니다. 로그는 stdout에 기록하거나 원격 로그 수집기로 전달해야 합니다.
이러한 애플리케이션은 읽기 전용 컨테이너 파일시스템을 가짐으로써 컨테이너의 공격 표면을 더욱 제한할 수 있습니다. 읽기 전용 파일시스템은 악의적인 사용자가 애플리케이션 구성을 변조하거나 추가 익스플로잇에 사용될 수 있는 추가 실행 파일을 디스크에 설치하는 것을 방지합니다.
flowchart TB
subgraph "컨테이너 파일시스템"
A[루트 파일시스템] -->|readOnlyRootFilesystem: true| B[읽기 전용]
C[emptyDir 볼륨] -->|/tmp 마운트| D[쓰기 가능]
E[emptyDir 볼륨] -->|/var/cache 마운트| F[쓰기 가능]
end
G[공격자] -.->|파일 쓰기 시도| B
B -.->|차단| H[Read-only file system 오류]
style B fill:#90EE90
style D fill:#FFE4B5
style F fill:#FFE4B5
readOnlyRootFilesystem: true를 설정하면 컨테이너의 루트 파일시스템이 읽기 전용으로 마운트됩니다. 이는 런타임에 컨테이너의 루트 파일시스템에 대한 모든 쓰기를 방지하고 불변 인프라 원칙을 강제합니다.
실패 케이스 (교육 목적)
apiVersion: v1
kind: Pod
metadata:
name: readonly-fail
spec:
containers:
- name: nginx
image: nginx:1.25
securityContext:
readOnlyRootFilesystem: true
테스트 결과: nginx는 /var/run, /var/cache/nginx에 쓰기가 필요하므로 오류가 발생합니다.
/docker-entrypoint.sh: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)
올바른 구현: 필요한 경로에 볼륨 마운트
apiVersion: v1
kind: Pod
metadata:
name: readonly-success
spec:
securityContext:
runAsUser: 101
runAsNonRoot: true
containers:
- name: nginx
image: nginx:1.25
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: var-run
mountPath: /var/run
- name: var-cache
mountPath: /var/cache/nginx
volumes:
- name: tmp
emptyDir: {}
- name: var-run
emptyDir: {}
- name: var-cache
emptyDir: {}
테스트 결과:
# 루트 파일시스템 쓰기 차단 확인
$ kubectl exec readonly-success -- touch /test.txt
touch: /test.txt: Read-only file system
# emptyDir 볼륨 쓰기 성공 확인
$ kubectl exec readonly-success -- touch /tmp/test.txt
(성공)
실무 팁: 애플리케이션이 어떤 경로에 쓰기가 필요한지 확인하려면 먼저 readOnlyRootFilesystem 없이 실행하고 로그를 확인하거나, strace를 사용하여 파일 I/O를 모니터링합니다.
4. Seccomp 프로파일
securityContext의 두 가지 추가 필수 확인 옵션은 seccompProfile과 seLinuxOptions입니다.
Seccomp(Secure Computing Mode)는 컨테이너에서 실행되는 프로세스가 사용 가능한 시스템 호출의 하위 집합만 호출하도록 제한하는 데 사용할 수 있는 Linux 커널 기능입니다. Linux 커널은 300개 이상의 시스템 콜을 제공하지만, 대부분의 애플리케이션은 소수의 시스템 콜만 사용합니다.
flowchart LR
A[300+ 시스템 콜] --> B{Seccomp 프로파일}
B -->|RuntimeDefault| C[안전한 시스템 콜만 허용]
B -->|Localhost| D[커스텀 정책]
B -->|Unconfined| E[모든 시스템 콜 허용]
F[애플리케이션] --> G[read, write, open...]
F --> H[unshare, mount...]
G --> C
H -.->|차단| C
style C fill:#90EE90
style E fill:#FFB6C1
apiVersion: v1
kind: Pod
metadata:
name: seccomp-pod
spec:
securityContext:
seccompProfile:
type: RuntimeDefault # 컨테이너 런타임의 기본 seccomp 프로파일 사용
containers:
- name: app
image: nginx
테스트 결과:
# 일반 명령은 정상 작동
$ kubectl exec seccomp-test -- ls /
bin dev etc home ...
# 위험한 시스템 콜은 차단
$ kubectl exec seccomp-test -- unshare --user true
unshare: unshare failed: Operation not permitted
unshare 시스템 콜은 네임스페이스를 생성하는 데 사용되며, 컨테이너 탈출에 악용될 수 있으므로 RuntimeDefault 프로파일에서 차단됩니다.
Seccomp 프로파일 타입
| 타입 | 설명 | 사용 사례 |
|---|---|---|
Unconfined |
제한 없음 (seccomp 비활성화) | 사용 금지 |
RuntimeDefault |
containerd/CRI-O 기본 프로파일 | 일반 애플리케이션 (권장) |
Localhost |
노드의 /var/lib/kubelet/seccomp/에 있는 커스텀 프로파일 |
특수한 요구사항이 있는 앱 |
커스텀 프로파일이 필요한 경우 노드의 /var/lib/kubelet/seccomp 디렉토리에 프로파일을 배치하고 Localhost 타입으로 참조할 수 있습니다.
# 커스텀 seccomp 프로파일 사용 예제
apiVersion: v1
kind: Pod
metadata:
name: custom-seccomp-pod
spec:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: profiles/custom-seccomp.json
containers:
- name: app
image: nginx
5. AppArmor 프로파일
seLinuxOptions는 Pod 내 모든 컨테이너와 볼륨에 사용자 정의 SELinux 레이블을 할당할 수 있습니다. 이와 유사하게 AppArmor(Application Armor)는 프로그램을 제한된 리소스 집합으로 제한하는 강제 접근 제어(MAC)를 제공하는 Linux 커널 보안 모듈입니다. 파일, capabilities, 네트워크 접근 등을 세밀하게 제어할 수 있습니다.
Kubernetes v1.30 이전에는 AppArmor가 어노테이션을 통해 구성되었습니다. 이제는 Pod 또는 Container의 securityContext.appArmorProfile 필드를 통해 설정됩니다.
# Kubernetes v1.30+ AppArmor 설정
apiVersion: v1
kind: Pod
metadata:
name: apparmor-pod
spec:
containers:
- name: app
image: nginx
securityContext:
appArmorProfile:
type: RuntimeDefault # 컨테이너 런타임의 기본 AppArmor 프로파일 사용
# Kubernetes v1.30 이전 (어노테이션 방식)
apiVersion: v1
kind: Pod
metadata:
name: apparmor-pod-legacy
annotations:
container.apparmor.security.beta.kubernetes.io/app: runtime/default
spec:
containers:
- name: app
image: nginx
AppArmor와 Seccomp의 차이점
| 특성 | Seccomp | AppArmor |
|---|---|---|
| 제어 대상 | 시스템 콜 | 파일, 네트워크, capabilities 등 |
| 제어 수준 | 시스템 콜 허용/차단 | 경로 기반 세밀한 접근 제어 |
| 지원 배포판 | 모든 Linux | Debian/Ubuntu 계열 |
| Kubernetes 통합 | GA (securityContext) | v1.30부터 GA |
6. Pod Security Standards와 Admission
PodSecurityPolicy는 Kubernetes v1.25에서 제거되고 Pod Security Admission(PSA) 컨트롤러로 대체되었습니다. PSA는 Pod Security Standards(PSS)를 기반으로 네임스페이스 수준에서 보안 정책을 시행합니다.
flowchart TB
subgraph "Pod Security Standards 계층"
A[Privileged] -->|제한 없음| B[인프라 워크로드]
C[Baseline] -->|기본 제한| D[일반 애플리케이션]
E[Restricted] -->|최대 제한| F[보안 중요 워크로드]
end
subgraph "제한 사항"
D --> G[privileged 차단]
D --> H[hostNetwork 차단]
F --> G
F --> H
F --> I[runAsNonRoot 필수]
F --> J[seccompProfile 필수]
F --> K[hostPath 차단]
end
Pod Security Standards 수준
| 수준 | 설명 | 주요 제한 사항 |
|---|---|---|
| Privileged | 제한 없음, 의도적으로 열림 | 없음 - 신뢰할 수 있는 사용자와 인프라 워크로드용 |
| Baseline | 일반 비핵심 워크로드용. 알려진 권한 상승을 방지하는 최소 제한 정책 | privileged 컨테이너 불가, 특정 capabilities 제한, hostNetwork/hostPID 차단 |
| Restricted | 보안 강화 모범 사례 적용. 보안 중요 애플리케이션 및 신뢰도가 낮은 사용자용 | non-root 필수, allowPrivilegeEscalation 금지, seccompProfile 필수, 볼륨 타입 제한 |
보안 표준은 레이블을 사용하여 Kubernetes 네임스페이스에 적용됩니다. 레이블은 표준 수준과 잠재적 위반이 감지되었을 때 취할 하나 이상의 조치를 정의합니다.
PSA 모드
| 모드 | 동작 | 용도 |
|---|---|---|
enforce |
정책 위반 시 Pod 생성 거부 | 프로덕션 환경 정책 강제 |
warn |
정책 위반 시 경고 표시 | 개발자에게 개선 사항 알림 |
audit |
정책 위반을 감사 로그에 기록 | 정책 영향도 분석 |
apiVersion: v1
kind: Namespace
metadata:
name: baseline-namespace
labels:
# Baseline 표준 위반 시 Pod 생성 거부
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.29
# Restricted 표준 위반 시 경고 표시
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.29
# Restricted 표준 위반을 감사 로그에 기록
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.29
이 구성은 baseline 표준을 만족하지 않는 모든 Pod를 거부하고, restricted 표준 요구사항을 충족하지 않는 Pod에 대해 경고를 생성합니다. 기존 네임스페이스의 구성을 업데이트하거나 하나 또는 모든 기존 네임스페이스에 정책을 적용하는 것도 가능합니다.
테스트 결과:
# Privileged Pod를 baseline 네임스페이스에 배포 시도
$ kubectl apply -f privileged-pod.yaml -n baseline-ns
Error: pods "privileged-pod" is forbidden: violates PodSecurity "baseline:latest":
privileged (container "app" must not set securityContext.privileged=true)
# Restricted Pod는 모든 네임스페이스에서 허용
$ kubectl apply -f restricted-pod.yaml -n restricted-ns
pod/restricted-pod created
레거시 애플리케이션 마이그레이션
Kubernetes 보안 제어를 고려하지 않고 구현되거나 컨테이너화된 레거시 애플리케이션을 실행하는 것은 Kubernetes의 일반적인 보안 과제 중 하나입니다. 엄격한 보안 정책이 적용된 Kubernetes 배포판이나 환경에서 privileged 컨테이너를 실행하는 것은 어려울 수 있습니다.
Init Container 패턴
Init Container에서 root 권한이 필요한 작업을 수행한 후, 애플리케이션 컨테이너는 non-root로 실행합니다.
sequenceDiagram
participant K as Kubernetes
participant I as Init Container (root)
participant A as App Container (non-root)
participant V as Volume
K->>I: 시작 (runAsUser: 0)
I->>V: 디렉토리 생성 및 권한 설정
I->>V: chown 1000:2000 /data
I->>K: 완료
K->>A: 시작 (runAsUser: 1000)
A->>V: 파일 읽기/쓰기
Note over A: root 권한 불필요
apiVersion: v1
kind: Pod
metadata:
name: legacy-app-migration
spec:
initContainers:
- name: fix-permissions
image: busybox:1.36
command:
- sh
- -c
- |
mkdir -p /data/app /data/logs
chown -R 1000:2000 /data
chmod -R 755 /data
securityContext:
runAsUser: 0 # Init container만 root로 실행
volumeMounts:
- name: app-data
mountPath: /data
containers:
- name: app
image: legacy-app:1.0
securityContext:
runAsUser: 1000
runAsNonRoot: true
allowPrivilegeEscalation: false
volumeMounts:
- name: app-data
mountPath: /data
volumes:
- name: app-data
emptyDir: {}
테스트 결과:
# Init Container 로그
=== Init Container: Setting up permissions ===
Initialization completed!
total 20
drwxr-xr-x 5 1000 2000 4096 Jan 3 09:49 .
# App Container 로그
uid=1000 gid=2000 groups=2000,3000
Testing write access to data directory:
Sat Jan 3 09:49:42 UTC 2026
Application is running securely as non-root user...
점진적 마이그레이션 전략
flowchart TB
A[현재 상태 분석] --> B[PSS warn 모드 적용]
B --> C[위반 사항 파악]
C --> D{수정 가능?}
D -->|Yes| E[보안 설정 추가]
D -->|No| F[Init Container 패턴]
E --> G[PSS enforce 적용]
F --> G
G --> H[프로덕션 배포]
단계별 접근:
- 현황 파악 (1-2주): 네임스페이스에
warn: baseline레이블을 적용하고 경고 메시지를 수집 및 분석합니다. - 우선순위 결정 (1주): 위반 사항을 보안 영향도별로 분류하고 수정 용이성을 평가합니다.
- 점진적 수정 (4-6주): 간단한 수정부터 시작합니다 (runAsNonRoot 추가 등). 복잡한 케이스는 init container 또는 이미지 재빌드를 고려합니다.
- 정책 강화 (1-2주):
enforce: baseline을 적용하고,warn: restricted설정으로 다음 단계를 준비합니다. - 모니터링 (지속적): 보안 이벤트를 모니터링하고, 정기적으로 정책을 검토합니다.
최신 Kubernetes 보안 기능
Security Profiles Operator
Security Profiles Operator는 Kubernetes 클러스터에서 SELinux, seccomp, AppArmor를 더 쉽게 사용할 수 있도록 하는 CNCF 프로젝트입니다. 실행 중인 워크로드에서 프로파일을 생성하고 Kubernetes 노드에 프로파일을 로드하는 기능을 제공합니다.
현재 Kubernetes는 seccomp, AppArmor, SELinux 프로파일을 노드에 로드하기 위한 네이티브 메커니즘을 제공하지 않습니다. 이들은 노드가 부트스트랩될 때 수동으로 로드하거나 설치해야 합니다. 스케줄러는 어떤 노드에 프로파일이 있는지 알지 못하므로, Pod에서 참조하기 전에 이 작업을 완료해야 합니다. Security Profiles Operator는 이 문제를 해결합니다.
# SeccompProfile CRD 예제
apiVersion: security-profiles-operator.x-k8s.io/v1beta1
kind: SeccompProfile
metadata:
name: custom-profile
namespace: default
spec:
defaultAction: SCMP_ACT_LOG # 모든 시스템 콜 로깅
syscalls:
- action: SCMP_ACT_ALLOW
names:
- read
- write
- exit
- sigreturn
# ProfileBinding으로 워크로드에 프로파일 연결
apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
kind: ProfileBinding
metadata:
name: nginx-binding
spec:
profileRef:
kind: SeccompProfile
name: custom-profile
image: nginx:*
User Namespaces (Kubernetes v1.25+)
User Namespaces는 컨테이너 내부의 root 사용자를 호스트의 비특권 사용자로 매핑하여 추가적인 격리 계층을 제공합니다.
apiVersion: v1
kind: Pod
metadata:
name: userns-pod
spec:
hostUsers: false # User Namespace 활성화
containers:
- name: app
image: nginx
프로덕션급 보안 설정
실무에서는 여러 보안 제어를 조합하여 심층 방어를 구현합니다.
apiVersion: v1
kind: Pod
metadata:
name: production-app
namespace: restricted-ns
spec:
# Pod 수준 보안 설정 (모든 컨테이너에 적용)
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 2000
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0
# 컨테이너 수준 보안 설정 (이 컨테이너에만 적용)
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ['ALL']
volumeMounts:
- name: tmp
mountPath: /tmp
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: tmp
emptyDir: {}
테스트 결과: 이 설정으로 실행된 Pod에서 다음을 확인했습니다.
- UID 1000으로 실행 (non-root)
- 모든 capabilities 제거됨 (CapEff=0)
- 루트 파일시스템 쓰기 차단
- seccomp으로 위험한 시스템 콜 차단
실습 가이드
환경 준비
# 테스트 네임스페이스 생성
kubectl create namespace pss-baseline
kubectl create namespace pss-restricted
# PSS 레이블 적용
kubectl label namespace pss-baseline \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted
kubectl label namespace pss-restricted \
pod-security.kubernetes.io/enforce=restricted
주요 검증 포인트
1. runAsNonRoot 검증
# Pod 생성 후 사용자 확인
kubectl exec POD_NAME -- id
# 출력: uid=1000 gid=2000 groups=2000,3000
2. Capabilities 검증
# Capabilities 확인
kubectl exec POD_NAME -- cat /proc/1/status | grep Cap
# CapEff: 0000000000000000 (모든 capabilities 제거)
3. 읽기 전용 파일시스템 검증
# 루트 파일시스템 쓰기 시도 (실패 예상)
kubectl exec POD_NAME -- touch /test.txt
# 출력: touch: /test.txt: Read-only file system
# emptyDir 볼륨 쓰기 (성공 예상)
kubectl exec POD_NAME -- touch /tmp/test.txt
4. Seccomp 검증
# 위험한 시스템 콜 차단 확인
kubectl exec POD_NAME -- unshare --user true
# 출력: unshare: unshare failed: Operation not permitted
테스트 결과 요약
전체 테스트 실행 결과:
| 테스트 카테고리 | 성공률 |
|---|---|
| SecurityContext | 100% (4/4) |
| Capabilities | 100% (4/4) |
| Filesystem | 100% (3/3) |
| Pod Security Standards | 87% (7/8) |
| Seccomp | 100% (3/3) |
| Integration | 100% (3/3) |
| 전체 | 92% (22/24) |
주요 발견 사항:
- runAsNonRoot: root 사용자 차단이 효과적으로 작동합니다.
- Capabilities: drop ALL 후 CapEff=0을 확인했습니다.
- readOnlyRootFilesystem: 루트 FS 쓰기가 차단되고, emptyDir 쓰기는 성공합니다.
- PSS: 최신 Kubernetes는 baseline에서도 hostPath를 차단합니다 (보안 강화).
- Init Container 패턴: 레거시 앱 보안 마이그레이션에 효과적입니다.
트러블슈팅
문제: Pod가 CrashLoopBackOff
증상: readOnlyRootFilesystem 설정 후 Pod가 반복적으로 재시작됩니다.
원인: 애플리케이션이 쓰기 권한이 필요한 디렉토리에 접근을 시도합니다.
해결책:
- 로그에서 쓰기 실패 메시지를 확인합니다.
kubectl logs POD_NAME | grep "Read-only file system"- 해당 경로에 emptyDir 볼륨을 마운트합니다.
volumeMounts: - name: app-cache mountPath: /path/to/writable/dir volumes: - name: app-cache emptyDir: {}
문제: "violates PodSecurity" 오류
증상: Pod 생성 시 PSS 정책 위반 오류가 발생합니다.
원인: Pod 스펙이 네임스페이스의 PSS 수준을 만족하지 못합니다.
해결책:
- 오류 메시지에서 위반 항목을 확인합니다.
violates PodSecurity "baseline:latest": privileged (container "app" must not set securityContext.privileged=true)- Pod 스펙을 수정하거나 적절한 네임스페이스로 변경합니다.
문제: Capabilities 부족으로 기능 동작 안 함
증상: drop ALL 설정 후 특정 기능이 작동하지 않습니다.
원인: 필요한 capability가 제거되었습니다.
해결책:
- 필요한 capability를 식별합니다 (애플리케이션 문서 참조).
- 최소한의 capability만 추가합니다.
capabilities: drop: ['ALL'] add: ['NET_BIND_SERVICE'] # 1024 이하 포트용
보안 설정 체크리스트
-
runAsNonRoot: true설정 -
allowPrivilegeEscalation: false설정 -
capabilities.drop: ['ALL']설정 -
readOnlyRootFilesystem: true설정 (가능한 경우) -
seccompProfile.type: RuntimeDefault설정 - 네임스페이스에 적절한 PSS 레벨 적용
- 리소스 requests/limits 설정
개발 환경 vs 프로덕션 환경
flowchart LR
subgraph "개발 환경"
A[PSS: privileged] --> B[빠른 개발]
A --> C[보안 경고 표시]
end
subgraph "스테이징 환경"
D[PSS: baseline enforce] --> E[보안 검증]
D --> F[restricted warn]
end
subgraph "프로덕션 환경"
G[PSS: restricted enforce] --> H[최대 보안]
G --> I[엄격한 정책]
end
A --> D
D --> G
이미지 빌드 시 보안 고려
Dockerfile에서 보안 설정을 미리 적용하면 Kubernetes 설정이 간소화됩니다.
FROM python:3.11-slim
# non-root 사용자 생성
RUN groupadd -r appuser -g 1000 && \
useradd -r -u 1000 -g appuser appuser
# 애플리케이션 디렉토리
WORKDIR /app
COPY --chown=appuser:appuser . /app
# 쓰기 가능한 디렉토리 권한 설정
RUN mkdir -p /app/tmp /app/logs && \
chown -R appuser:appuser /app/tmp /app/logs
# non-root 사용자로 전환
USER appuser
CMD ["python", "app.py"]
모니터링과 감사
보안 설정이 실제로 작동하는지 지속적으로 모니터링합니다.
# PSS 위반 감사 로그 조회 (Kubernetes 감사 로그 활성화 필요)
kubectl get events --all-namespaces | grep "violates PodSecurity"
# 특권 컨테이너 검색
kubectl get pods --all-namespaces -o json | \
jq -r '.items[] | select(.spec.containers[].securityContext.privileged==true) |
"\(.metadata.namespace)/\(.metadata.name)"'
# root로 실행 중인 Pod 검색
kubectl get pods --all-namespaces -o json | \
jq -r '.items[] | select(.spec.securityContext.runAsNonRoot!=true) |
"\(.metadata.namespace)/\(.metadata.name)"'
Discussion
보안 고려사항과 테스트 실습을 개발 단계에서 Kubernetes 프로덕션 보안 표준으로 배포하는 것을 포함하는 Shift Left 경향이 점점 더 대중화되고 있습니다. 이러한 실습은 개발 주기 초기에 보안 문제를 식별하고 해결하여 마지막 순간의 놀라움을 방지하는 데 도움이 됩니다.
Shift Left는 나중에 하기보다 먼저 하는 것에 관한 것입니다. 개발 및 배포 프로세스를 설명하는 시간선에서 왼쪽으로 가는 것입니다. 우리의 맥락에서 Shift Left는 개발자가 애플리케이션을 개발할 때 이미 운영 보안에 대해 생각하는 것을 의미합니다.
이 장에서 우리는 안전한 클라우드 네이티브 애플리케이션을 만들 때 충분한 고려 대상을 제공하길 바랍니다. 이 장의 지침은 로컬 파일시스템에 쓰지 않거나 root 권한을 필요로 하지 않는 애플리케이션을 설계하고 구현하는 데 도움이 될 것입니다. 예를 들어 애플리케이션을 컨테이너화할 때 컨테이너에 지정된 non-root 사용자가 있는지 확인하고 security context를 구성하는 것입니다.
애플리케이션이 정확히 무엇을 필요로 하는지 이해하고 최소한의 권한만 부여하시기 바랍니다. 또한 워크로드와 호스트 사이에 경계를 구축하고, 컨테이너 권한을 줄이고, 침해 시 리소스 활용을 제한하도록 런타임 환경을 구성하는 것을 목표로 했습니다. 이 노력에서 Process Containment 패턴은 보안 침해를 포함하여 "컨테이너에서 일어난 일은 컨테이너에 머물게" 보장합니다.
참고 자료
- Configure a Security Context for a Pod or Container
- Pod Security Standards
- Pod Security Admission
- Enforce Pod Security Standards with Namespace Labels
- Linux Capabilities
- Seccomp Security Profiles
- Security Profiles Operator
- AWS EKS Best Practices - Runtime Security
- 10 Kubernetes Security Context Settings You Should Understand
- Security Risk Analysis Tool for Kubernetes Resources
'k8s > kubernetes-pattern' 카테고리의 다른 글
| Kubernetes Pattern: Secure Configuration (0) | 2026.01.17 |
|---|---|
| Kubernetes Pattern: Network Segmentation (0) | 2026.01.10 |
| Kubernetes Pattern: Configuration Template (0) | 2025.12.27 |
| Kubernetes Pattern: Immutable Configuration & Kubernetes Churn (0) | 2025.12.27 |
| Kubernetes Pattern: EnvVar Configuration && Configuration Resource (0) | 2025.12.20 |