클러스터 노드와 네트워크 보안
- 13.1 호스트 네임스페이스를 파드에서 쓰기
- 13.2 컨테이너 SecurityContext 설정
- 13.3 PodSecurityPolicy로 클러스터 차원 제한
- 13.4 NetworkPolicy로 파드 네트워크 격리
12장이 "API 서버에 누가 접근하느냐"를 다뤘다면, 13장은 한 단계 안쪽 — 이미 API 서버를 통과한 파드가 노드에 어떤 짓을 할 수 있는가, 그리고 파드끼리 어떻게 서로 격리할 것인가의 문제야. 컨테이너는 이름과 달리 그렇게 안 격리돼 있거든. privileged: true 한 줄이면 사실상 노드 root야.
리눅스 컨테이너 격리의 본질은 **네임스페이스(network, PID, IPC, mount, user, UTS)**야. 파드는 보통 자기만의 네임스페이스를 갖지만, 시스템 파드는 호스트 것을 빌려야 할 때가 있어. **hostNetwork: true**는 파드가 노드의 네트워크 인터페이스를 그대로 쓰는 거야. 자기 IP가 없고, 컨테이너가 바인딩하는 포트는 노드 포트가 돼. kubeadm으로 깐 클러스터에서 control plane 컴포넌트들이 이렇게 떠 있어. **hostPort**는 네트워크 네임스페이스는 자기 거지만 노드의 특정 포트에 직접 매핑돼. NodePort Service와 헷갈리지 마 — NodePort는 모든 노드에 포트를 열고 임의 파드로 분산하지만, hostPort는 그 파드가 떠 있는 노드의 포트만 열고 그 파드로 직결돼. 같은 hostPort를 쓰는 파드는 노드당 1개만 스케줄돼. **hostPID: true**나 **hostIPC: true**는 노드의 모든 프로세스를 보거나 IPC로 통신할 수 있게 해줘.
securityContext는 파드 레벨과 컨테이너 레벨 둘 다 설정 가능하고 컨테이너 레벨이 우선이야. **runAsUser**는 이미지의 USER 디렉티브가 정한 사용자를 덮어쓸 수 있고, **runAsNonRoot: true**가 특히 중요해 — 공격자가 이미지 레지스트리를 뚫어서 동일 태그로 root 이미지를 푸시했을 때도 막아주거든. Kubelet이 파드를 스케줄해도 컨테이너 시작 자체를 거부해. 호스트 디렉토리를 마운트할 때도 root면 전체 접근이지만 비root면 파일 권한이 적용되고. **privileged: true**는 노드 커널에 풀 액세스야. /dev 아래 모든 디바이스가 보이고. kube-proxy가 iptables를 만져야 해서 이렇게 떠. 하지만 보통은 너무 과해. 더 안전한 방식은 capability를 골라서 추가·제거하는 거야. securityContext.capabilities.add에 SYS_TIME(CAP_ 접두사는 빼고) 같은 걸 적고, drop에 CHOWN 같은 걸 적는 식. 리눅스는 root 권한을 수십 개 capability로 쪼개놨거든. SYS_TIME은 시스템 시간 변경, CHOWN은 파일 소유자 변경, SYS_ADMIN은 광범위한 관리 작업, SYS_MODULE은 커널 모듈 로드. 필요한 것만 주는 게 원칙이야.
**readOnlyRootFilesystem: true**는 컨테이너 루트 파일시스템을 읽기 전용으로 잠가. PHP 같이 코드 인젝션 취약점이 있는 앱이 자기 코드 파일을 못 덮어쓰게 만드는 게 핵심이야. 로그·캐시 같은 쓰기가 필요한 디렉토리는 emptyDir 볼륨을 마운트하면 돼. 운영 권장사항으로, 프로덕션 파드는 무조건 readOnlyRootFilesystem: true로 시작하는 게 맞아. **fsGroup**과 **supplementalGroups**는 서로 다른 user ID로 도는 두 컨테이너가 볼륨을 공유할 때 파일 권한 문제를 푸는 장치야. fsGroup은 마운트된 볼륨의 그룹 소유자를 설정해서, 두 컨테이너가 그 그룹에 속하면 같이 읽고 쓸 수 있게 해줘. supplementalGroups는 추가 그룹을 부여하고.
위의 모든 옵션은 누구나 파드 YAML에 적을 수 있어. 그러면 누가 막느냐? PodSecurityPolicy admission plugin이야. API 서버에 들어오는 파드 정의를 검사해서 거부하거나 기본값을 주입해. 참고로 PodSecurityPolicy는 K8s 1.21에서 deprecated되고 1.25에서 제거됐어. 후속은 Pod Security Admission이나 OPA/Gatekeeper, Kyverno 같은 외부 정책 엔진. 책 시점 기준이지만 개념은 그대로 유효해.
PodSecurityPolicy는 cluster-level 리소스고, hostNetwork·hostPID·hostIPC 사용 가능 여부, 바인딩 가능한 hostPort 범위, 허용된 user/group ID, privileged 컨테이너 허용 여부, 기본 추가·필수 제거할 capability, readOnlyRootFilesystem 강제 여부, 사용 가능한 volume 타입 같은 걸 정의해. runAsUser 룰엔 세 가지가 있어. **RunAsAny**는 제한 없음, **MustRunAs**는 정의된 ID 범위 안이어야 하고 파드가 명시 안 했으면 첫 번째 ID 주입, **MustRunAsNonRoot**는 0(root)이면 거부 — 이미지에 USER가 박혀 있어도 검사해. capabilities 세 필드의 차이도 짚어두면, **allowedCapabilities**는 파드가 직접 add할 수 있는 목록(whitelist), **defaultAddCapabilities**는 모든 컨테이너에 자동 추가, **requiredDropCapabilities**는 모든 컨테이너에서 자동 제거(drop을 강제)야.
PSP는 cluster-level이지만 RBAC으로 사용자별로 다르게 줄 수 있어. 패턴은 ClusterRole을 만들고 --verb=use --resource=podsecuritypolicies --resource-name=<psp-name>으로 특정 PSP를 가리키고, ClusterRoleBinding으로 사용자·그룹에 연결해. 기본 PSP는 system:authenticated 그룹에 바인딩(모두에게)하고, 권한 있는 PSP는 특정 사용자에만 바인딩해. use라는 verb가 핵심이야 — get/list가 아니라 "이 PSP를 적용받을 자격이 있다"는 뜻이거든.
마지막으로 NetworkPolicy. 기본적으로 같은 클러스터의 모든 파드는 모든 파드와 통신 가능해. 이걸 잠그는 게 NetworkPolicy야. 단, CNI 플러그인이 NetworkPolicy를 지원해야 동작해 (Calico, Cilium, Weave 같은 거). 지원 안 하는 플러그인이면 만들어도 무시돼. 그리고 한 가지 헷갈리지 말아야 할 건, 여기서 말하는 ingress는 5장의 Ingress 리소스와 전혀 다른 개념이야. NetworkPolicy의 ingress·egress는 그냥 들어오는·나가는 트래픽 규칙이야.
기본 패턴은 default-deny부터 시작이야. kind: NetworkPolicy에 podSelector: {}(빈 셀렉터 = ns의 모든 파드)만 두면 해당 네임스페이스의 모든 파드는 들어오는 연결이 다 막혀. 그 다음 명시적으로 풀어줘. 파드 → 파드 허용은 podSelector로 대상 파드를 정하고 ingress.from에 podSelector로 출발지 파드를 명시해. 예를 들어 app=database 파드는 app=webserver 파드로부터 5432 포트로만 받게 만들 수 있어. Service를 거쳐도 NetworkPolicy는 그대로 적용돼 — IP 기반이거든. 네임스페이스 단위는 namespaceSelector로 표현해. 멀티테넌트 클러스터에서 "manning 테넌트의 모든 ns에서만 접근 허용" 같은 규칙을 만들 수 있어. 멀티테넌트에서 한 가지 주의 — 테넌트가 자기 ns에 마음대로 라벨을 못 붙이게 해야 해. 안 그러면 namespaceSelector를 우회당해. CIDR 기반은 ipBlock.cidr로 표현하고, 외부 IP나 노드 IP 범위를 다룰 때 써. 마지막으로 egress는 ingress의 거울 — 나가는 방향 제어야. 웹서버 파드가 DB 파드 외엔 어디로도 못 나가게 만들 수 있어. 데이터 유출 방어의 기본 도구야.
정리
13장 읽고 기억할 거 세 가지:
- 컨테이너는 자동으로 안전하지 않다.
runAsNonRoot: true,readOnlyRootFilesystem: true, capability 최소화는 운영 파드의 기본값이어야 한다.privileged: true는 진짜 필요한 시스템 파드(kube-proxy 류)에만.runAsNonRoot는 이미지 공급망 공격(레지스트리 변조) 대비책으로도 유효하다. - PSP/Pod Security Admission이 없으면 SecurityContext는 자율 신고다. 누구나 privileged 파드를 만들 수 있다는 뜻. 클러스터 레벨에서 admission으로 강제하고, RBAC +
useverb로 사용자별로 차등 적용해야 한다. (책의 PSP는 1.25에서 제거됨 — 지금은 Pod Security Admission, Kyverno, OPA/Gatekeeper로 대체) - NetworkPolicy는 default-deny부터 시작한다. 기본은 fully open이라는 걸 잊으면 안 된다. 빈 podSelector로 ns 전체를 차단한 뒤 필요한 ingress/egress만 명시. 단 CNI가 지원해야 동작 — 만들기만 하고 효과 없으면 플러그인부터 의심하라.