Certified Kubernetes Security Specialist (CKS) #2 NetworkPolicy 깊이: default deny, ingress/egress (Cluster Setup)
CKS #1에서 시험 환경과 6개 도메인의 큰 그림을 잡았습니다. 이제 첫 도메인인 Cluster Setup으로 들어갑니다. 이 도메인의 핵심은 네트워크 격리이고, 그 도구가 바로 NetworkPolicy입니다. 쿠버네티스는 기본적으로 모든 Pod가 서로 자유롭게 통신할 수 있는 평평한 네트워크입니다. 공격자가 하나의 Pod를 장악하면 클러스터 전체로 횡적 이동(lateral movement)이 가능하다는 뜻입니다. NetworkPolicy는 이 평평한 네트워크를 작은 구획으로 쪼개, 필요한 통신만 남기고 나머지를 막는 방화벽입니다.
NetworkPolicy의 기본기는 CKA #20과 K8s 중급 #7에서 다뤘습니다. 이번 글은 그 위에서 CKS가 실제로 요구하는 깊이, 즉 default deny 설계, egress와 DNS의 함정, 셀렉터 조합의 AND,OR 차이에 집중하겠습니다.
NetworkPolicy의 기본 동작부터 다시 #
CKS에서 NetworkPolicy를 정확히 다루려면 먼저 기본 동작 두 가지를 헷갈리지 않아야 합니다.
정책이 없으면 all-allow #
NetworkPolicy가 하나도 없는 네임스페이스에서는 모든 Pod가 모든 방향으로 자유롭게 통신합니다. 이것이 쿠버네티스의 기본값입니다. 격리는 명시적으로 켜야 하는 기능이지, 처음부터 켜져 있는 것이 아닙니다.
정책이 붙은 Pod는 화이트리스트 #
어떤 Pod에 NetworkPolicy가 하나라도 매칭되는 순간, 그 Pod의 해당 방향(ingress 또는 egress) 트래픽은 화이트리스트 방식으로 바뀝니다. 정책에 명시적으로 허용한 트래픽만 통과하고, 나머지는 전부 막힙니다. 여기서 중요한 규칙이 두 가지입니다.
- 정책은 additive합니다. 같은 Pod에 여러 정책이 매칭되면, 각 정책이 허용하는 트래픽의 합집합이 허용됩니다. NetworkPolicy에는 deny 규칙 자체가 없고, 오직 allow의 합집합만 존재합니다.
- 한 Pod에 ingress 정책만 붙으면 egress는 여전히 all-allow입니다. 방향은 독립적으로 통제됩니다. 그래서
policyTypes필드로 이 정책이 어느 방향을 통제하는지 명확히 선언하는 것이 CKS의 핵심입니다.
이 두 규칙에서 default deny 패턴이 나옵니다. 어떤 트래픽도 허용하지 않는 정책을 모든 Pod에 매칭시키면, 합집합의 출발점이 “전부 차단"이 됩니다.
default deny: 전부 막고 시작하기 #
CKS 시험과 실무에서 가장 자주 쓰는 출발점은 네임스페이스 전체에 default deny를 깔고, 필요한 통신만 별도 정책으로 여는 것입니다. podSelector: {}는 네임스페이스의 모든 Pod를 선택하고, policyTypes에 막을 방향을 적습니다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: secure
spec:
podSelector: {} # 네임스페이스의 모든 Pod에 적용
policyTypes:
- Ingress
- Egress
# ingress/egress 규칙이 없으므로 양방향 전부 차단ingress와 egress 키 자체가 없으면 해당 방향으로 허용되는 트래픽이 하나도 없다는 뜻입니다. policyTypes에 두 방향을 모두 적었으므로, 이 네임스페이스의 모든 Pod는 들어오는 트래픽과 나가는 트래픽이 전부 막힙니다.
방향별로 따로 거는 패턴도 알아 두면 시험에서 유용합니다.
# ingress만 default deny (egress는 그대로 all-allow)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: secure
spec:
podSelector: {}
policyTypes:
- IngresspolicyTypes에 Ingress만 적고 ingress 규칙을 비워 두면, 들어오는 트래픽은 전부 막히되 나가는 트래픽은 통제하지 않습니다. “이 네임스페이스로 들어오는 트래픽만 잠가라” 유형에 그대로 쓰입니다.
ingress 제한: 누구에게서 들어오게 할 것인가 #
default deny를 깐 뒤에는 필요한 트래픽을 여는 정책을 추가합니다. ingress 규칙의 from은 출발지를, ports는 도착 포트를 지정합니다. 출발지를 고르는 셀렉터는 세 종류입니다.
podSelector: 같은 네임스페이스 안에서 label로 출발지 Pod를 고름namespaceSelector: 특정 label을 가진 네임스페이스 전체를 출발지로 고름ipBlock: CIDR로 IP 대역을 고름. 클러스터 밖 출발지에 사용
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: secure
spec:
podSelector:
matchLabels:
app: api # 이 정책은 app=api Pod로 들어오는 트래픽을 통제
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # app=frontend Pod에서 오는 트래픽만
ports:
- protocol: TCP
port: 8080이 정책은 app=api Pod에 대해, 같은 네임스페이스의 app=frontend Pod에서 TCP 8080으로 들어오는 트래픽만 허용합니다. default deny가 함께 깔려 있다면, 그 외의 모든 ingress는 차단된 상태이므로 이 한 줄이 곧 화이트리스트가 됩니다.
egress 제한: DNS의 함정 #
egress 규칙은 to로 도착지를, ports로 도착 포트를 지정합니다. 구조는 ingress와 대칭이지만, egress를 default deny로 막는 순간 거의 모든 통신이 깨지는 함정이 하나 있습니다. 바로 DNS입니다.
Pod가 api.secure.svc.cluster.local 같은 Service 이름으로 통신하려면, 먼저 그 이름을 IP로 바꾸는 DNS 조회를 해야 합니다. 이 조회는 CoreDNS로 가는 53번 포트(UDP와 TCP) 트래픽입니다. egress default deny를 깔면 이 DNS 트래픽까지 막혀 버려, Pod는 어떤 이름도 해석하지 못하고 모든 연결이 실패합니다. 목적지 IP를 직접 허용했더라도 이름을 IP로 바꾸지 못하면 소용이 없습니다.
그래서 egress를 잠글 때는 DNS 허용을 함께 넣는 것이 사실상 필수입니다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: secure
spec:
podSelector: {} # 네임스페이스의 모든 Pod
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns # CoreDNS Pod
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53kubernetes.io/metadata.name은 쿠버네티스가 모든 네임스페이스에 자동으로 붙여 주는 label이므로, CoreDNS가 사는 kube-system을 안전하게 고를 수 있습니다. DNS는 보통 UDP 53을 쓰지만 큰 응답은 TCP 53으로 넘어가므로, 시험에서는 두 프로토콜을 모두 허용하는 것이 안전합니다.
여기에 업무용 egress를 별도 정책으로 추가하면, “이 네임스페이스는 DNS와 특정 백엔드로만 나갈 수 있다"는 격리가 완성됩니다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-to-db
namespace: secure
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432default deny egress, allow-dns, allow-egress-to-db 세 정책이 함께 매칭되면, app=api Pod의 나가는 트래픽은 CoreDNS의 53번과 app=postgres의 5432번으로만 제한됩니다. 합집합 규칙 덕분에 이렇게 작은 정책을 쌓아 올리는 설계가 가능합니다.
셀렉터 조합: AND vs OR 함정 #
CKS에서 가장 실수가 잦은 지점이 namespaceSelector와 podSelector를 함께 쓸 때의 의미입니다. 한 항목 안에 두 셀렉터를 같이 쓰면 AND, 별도 항목으로 나누면 OR입니다. YAML의 들여쓰기 한 칸 차이로 의미가 정반대가 됩니다.
# (A) AND: prod 네임스페이스 "안에 있으면서" app=client인 Pod에서만 허용
ingress:
- from:
- namespaceSelector:
matchLabels:
env: prod
podSelector:
matchLabels:
app: client위는 from 리스트의 한 원소 안에 두 셀렉터가 들어 있습니다. 그래서 “env=prod 네임스페이스에 속하면서 동시에 app=client label을 가진 Pod"라는 교집합 조건이 됩니다. 대시(-)가 하나뿐임에 주목합니다.
# (B) OR: prod 네임스페이스의 모든 Pod "또는" 어디든 app=client인 Pod에서 허용
ingress:
- from:
- namespaceSelector:
matchLabels:
env: prod
- podSelector:
matchLabels:
app: client위는 from에 두 원소가 들어 있어, “env=prod 네임스페이스의 모든 Pod” 또는 “현재 네임스페이스에서 app=client인 Pod” 중 하나만 맞아도 허용됩니다. 의도보다 훨씬 넓게 열리는 흔한 사고이므로, 대시 개수로 AND인지 OR인지 반드시 확인합니다.
ipBlock과 except #
ipBlock은 CIDR 대역을 허용하되, except로 그 안의 일부를 다시 빼낼 수 있습니다.
ingress:
- from:
- ipBlock:
cidr: 10.0.0.0/16
except:
- 10.0.5.0/24 # 이 서브넷만 제외10.0.0.0/16 전체를 허용하되 10.0.5.0/24는 막습니다. 특정 신뢰 대역만 열되 그 안의 위험 구간을 도려낼 때 씁니다. except의 대역은 반드시 cidr 범위 안에 포함되어야 합니다.
시험 단골 시나리오 두 가지 #
CKS에서 반복해 나오는 NetworkPolicy 유형은 정형화되어 있습니다. 손이 먼저 기억하도록 두 유형을 정리하겠습니다.
유형 1: 네임스페이스를 격리하되 DNS만 허용 #
“secure 네임스페이스를 외부와 격리하되, DNS 해석은 가능하게 하라"는 유형입니다. default deny와 allow-dns 두 정책을 함께 적용합니다. 위에서 만든 default-deny-all과 allow-dns의 조합이 정확히 이 답입니다. egress를 막을 때 DNS를 함께 여는 것을 잊지 않는 것이 채점 포인트입니다.
유형 2: 특정 label에서만 ingress #
“app=db Pod로는 app=app Pod에서만 접속할 수 있게 하라"는 유형입니다. app=db를 대상으로 default-deny-ingress를 깔고, app=app에서 오는 ingress만 여는 정책을 추가합니다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-to-db
namespace: secure
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: appports를 생략하면 모든 포트가 허용되므로, 문제에서 포트를 지정했다면 반드시 ports를 넣습니다.
검증: 통신 테스트로 확인하기 #
정책을 적용했으면 실제로 막히는지,열리는지를 테스트해야 합니다. 채점은 정책의 효과로 이뤄지므로, YAML만 맞고 동작이 다르면 점수가 없습니다. 일회용 Pod로 직접 연결을 시도해 봅니다.
# 적용된 정책 목록 확인
kubectl get networkpolicy -n secure
# 특정 정책의 상세 규칙 확인
kubectl describe networkpolicy default-deny-all -n secure# secure 네임스페이스에서 임시 Pod로 api Service에 접속 시도
kubectl run test -n secure --rm -it --image=busybox --restart=Never -- \
wget -qO- --timeout=3 http://api:8080
# DNS 해석만 따로 확인
kubectl run test -n secure --rm -it --image=busybox --restart=Never -- \
nslookup apiwget이 타임아웃되면 ingress가 막힌 것이고, 응답이 오면 열린 것입니다. nslookup이 실패하면 DNS egress가 막혀 있다는 신호이므로 allow-dns 정책을 점검합니다. 단, 임시 Pod에 정책이 매칭되려면 그 Pod의 label과 네임스페이스가 정책 조건에 맞아야 하므로, 출발지 label을 정확히 맞춰 띄우는 것이 중요합니다.
한 가지 주의할 점은 NetworkPolicy를 실제로 강제하는 것은 CNI 플러그인이라는 사실입니다. Calico, Cilium 같은 정책 지원 CNI가 깔려 있어야 효과가 납니다. 일부 환경에서는 정책을 적용해도 통제가 일어나지 않을 수 있으므로, 시험 환경의 CNI가 NetworkPolicy를 지원한다는 전제 위에서 작업합니다.
시험 포인트 #
- 기본 동작. 정책이 없으면 all-allow. 정책이 매칭되면 해당 방향은 화이트리스트. NetworkPolicy에 deny 규칙은 없고 allow의 합집합만 존재합니다.
- default deny.
podSelector: {}로 모든 Pod를 고르고,policyTypes에 막을 방향을 적습니다.ingress,egress키가 없으면 그 방향은 전부 차단입니다. - DNS 함정. egress를 default deny로 막으면 53번 포트가 막혀 이름 해석이 깨집니다. allow-dns 정책으로
kube-system의 CoreDNS에 UDP,TCP 53을 함께 엽니다. - AND vs OR.
from,to의 한 항목 안에 두 셀렉터를 쓰면 AND, 별도 항목으로 나누면 OR입니다. 대시 개수로 구분합니다. - ipBlock,except. CIDR로 외부 대역을 허용하고,
except로 그 안의 일부를 제외합니다. - 검증. 임시 Pod로 통신을 직접 테스트합니다. 강제 주체는 CNI 플러그인입니다.
정리 #
NetworkPolicy는 평평한 쿠버네티스 네트워크를 구획으로 쪼개는 화이트리스트 방화벽입니다. default deny로 전부 막고, 필요한 통신만 작은 정책으로 더해 가는 설계가 CKS의 정답 패턴입니다. egress를 막을 때는 DNS를 함께 여는 것을 잊지 말고, 셀렉터를 조합할 때는 AND와 OR을 들여쓰기로 정확히 구분하며, 마지막에는 임시 Pod로 효과를 검증합니다. 이 세 가지만 손에 익으면 Cluster Setup의 네트워크 격리 작업은 안정적으로 점수가 됩니다.
다음: CIS benchmark #
네트워크 격리를 잡았으니, 다음은 클러스터 자체의 설정을 점검하는 차례입니다.
#3 CIS benchmark (kube-bench), 컴포넌트 보안, Ingress TLS, 바이너리 검증에서는 kube-bench로 CIS benchmark를 자동 점검하는 법, API server,kubelet,etcd 같은 컴포넌트의 보안 설정을 점검 항목에 맞춰 고치는 법, Ingress에 TLS를 적용하는 법, 그리고 다운로드한 바이너리의 해시,서명을 검증하는 법까지 직접 실행해 보며 정리하겠습니다.