NetworkPolicy 하나 넣었더니 모든 트래픽이 막혔다 — "기본 허용" 함정
한 팀이 보안 점검을 앞두고 NetworkPolicy를 도입했다. "웹 Pod는 DB Pod로만 가게 하자"며 정책을 하나 넣었다. 그랬더니 갑자기 다른 트래픽 — 웹 Pod가 외부 API를 부르던 것, 모니터링이 메트릭을 수집하던 것 — 이 전부 멈췄다. "정책을 하나만 넣었는데 왜 다 막혔지?" 이 팀이 부딪힌 것이 NetworkPolicy의 "기본 허용, 정책 시 화이트리스트" 모델이다.
이 글이 푸는 것은: NetworkPolicy가 기본으로 모든 트래픽을 허용하는 이유, 하나의 정책이 왜 전체를 막는가, 그리고 L3/L4 격리의 한계다.
왜 Kubernetes 네트워크 기본이 "전부 허용"인가
대부분의 방화벽은 기본이 거부다 — 명시적으로 열지 않으면 닫혀 있다. 그런데 Kubernetes 네트워크는 반대다. NetworkPolicy가 하나도 없으면, 모든 Pod가 다른 모든 Pod로, 그리고 외부로 자유롭게 통신한다. 전혀 "보안 기본"이 아니다.
왜 이렇게 설계됐을까? Kubernetes의 철학이 연결성 우선이기 때문이다. 클러스터에 앱을 올리면 "서로를 찾고 통신할 수 있어야" 동작한다 — Service가 Pod를 찾고, 클라이언트가 Service를 부르고. 이 기본 연결성을 *거부로 두면, 아무 설정 안 한 클러스터는 아무것도 안 통신하는 사태가 벌어진다. Kubernetes는 "일단 다 되게" 시작하고, *격리가 필요한 곳만 정책으로 닫는 쪽을 택했다.
이 선택의 결과: 네트워크 보안은 사용자가 명시적으로 켜야 한다. Pod는 서로를 자유롭게 때릴 수 있고, 격리가 필요하면 NetworkPolicy로 닫는다. 이것이 NetworkPolicy가 존재하는 이유이자, "정책 하나 넣었더니 다 막혔다" 사태의 출발점이다. (Kubernetes docs - Network Policies)
하나의 정책이 "화이트리스트 모드"를 켠다
이제 핵심 규칙을 보자. 어떤 Pod에 대해 하나라도 정책이 해당 방향(ingress/egress)을 선택하면, 그 방향은 선택된 정책으로만 허용된다. 즉, 그 방향이 화이트리스트 모드로 바뀐다 — 명시된 것만 허용되고 나머지는 거부된다.
flowchart TD
Q1{"이 Pod에 ingress를<br/>선택하는 정책이 있는가?"} -->|"없다"| ALL1["ingress: 전부 허용(기본)"]
Q1 -->|"있다"| WL1["ingress: 정책에 명시된 것만 허용<br/>나머지는 *거부*"]
Q2{"egress도 같은 논리"} --> ALL2["전부 허용"]
Q2 --> WL2["정책 명시만 허용"]
이것이 "정책 하나 넣었더니 다 막혔다"의 원인이다. 도입의 팀이 웹 Pod에 ingress 정책을 넣으면 — 웹 Pod의 ingress는 이제 그 정책에 적힌 것만 받는다. 만약 egress 정책도 함께 넣었다면 그쪽도 화이트리스트로 바뀌어, 외부 API 호출(egress)도 막힌다.
ingress와 egress는 독립적이다. ingress 정책만 있으면 egress는 여전히 "전부 허용". 둘 다 넣어야 양방향 격리. 이 독립성을 모르면 "어느 한쪽만 막힌다"는 혼란이 생긴다.
NetworkPolicy 구조 — 두 개의 podSelector가 각각 무슨 일을 하나
# Kubernetes 1.36
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: web-to-db, namespace: app}
spec:
podSelector: {matchLabels: {app: db}} # (1) 이 정책이 *통제하는* Pod
policyTypes: [Ingress, Egress]
ingress:
- from:
- podSelector: {matchLabels: {app: web}} # (2) db가 *받을* 출처
ports: [{protocol: TCP, port: 5432}]
egress:
- to:
- podSelector: {matchLabels: {app: web}} # (3) db가 *나갈* 곳 (예시)
ports: [{protocol: TCP, port: 5432}]
처음 읽으면 헷갈리는 부분이 podSelector가 두 번 나온다는 것이다. 이 둘은 역할이 다르다:
- spec.podSelector(최상위): 이 정책이 통제하는 대상. "이 정책은 누구에게 적용되는가"의 답. 위에서는
app: db— db Pod가 통제 대상. - ingress.from.podSelector / egress.to.podSelector: 통제 대상이 받아들이거나 나갈 수 있는 상대. "누구로부터/누구에게 허용하는가"의 답. 위에서는
app: web— web Pod만 db에게 접근 허용.
이 분리를 이해하면 정책을 읽는 법이 보인다: "이 정책은 [최상위 podSelector]에게 적용되며, [from/to podSelector]에서 오는/가는 트래픽만 허용한다."
나머지 요소:
- policyTypes: Ingress/Egress/둘 다 중 어느 방향을 제어할지.
- ports: 허용할 포트/프로토콜. 생략하면 모든 포트.
기본 거부(default-deny) 정책 — 빈 정책의 힘
격리를 시작하는 가장 단순한 정책은 아무것도 허용하지 않는 것이다:
# Kubernetes 1.36
spec:
podSelector: {} # namespace의 모든 Pod
policyTypes: [Ingress, Egress]
# ingress/egress 생략 = 허용 항목 없음 = 전부 거부
이 정책은 namespace의 모든 Pod에 대해 ingress·egress를 전부 거부로 만든다(빈 허용 목록 = 아무것도 허용 안 함). 이후 허용하고 싶은 것을 별도 정책으로 추가하는 패턴이 "default-deny + whitelist"의 표준이다. 전부 닫고 시작해, 필요한 만큼만 여는 구조.
NetworkPolicy의 한계 — Calico CRD가 넘는 지점
Kubernetes 기본 NetworkPolicy엔 한계가 있다:
- 거부(deny) 규칙이 없다. 모든 규칙은 "허용". "이 IP만 거부"를 직접 못 쓴다(화이트리스트로 우회해야).
- namespace 간 정책이 불편.
namespaceSelector로 가능하지만, "전체 클러스터에 적용"은 어렵다(namespace마다 따로). - L7(HTTP 경로, 메서드) 불가. L3/L4(IP, 포트)만.
- 로그가 안 남는다. 정책에 막힌 패킷이 조용히 버려진다.
이 한계를 넘는 것이 07장의 Calico CRD(GlobalNetworkPolicy) — deny 규칙, L7, 글로벌 정책. Cilium의 CRD도 비슷하다. 기본 NetworkPolicy가 부족하면 CRD 정책으로 올라간다.
정책은 구현체에 따라 동작이 다르다
NetworkPolicy는 API 규격이다. 실제 동작은 CNI 구현체(Calico/Cilium 등)가 결정한다. 이 점을 놓치면 "정책을 넣었는데 아무 효과가 없다"는 사태를 겪는다.
- 지원 여부: Flannel(기본)은 NetworkPolicy를 지원하지 않는다. Calico/Cilium은 지원. CNI가 지원하지 않으면 정책이 무시된다 — 적어도 아무 효과 없음.
- 평가 지점: Calico는 felix가 커널(iptables/eBPF)에 규칙을 깐다. Cilium은 eBPF가 커널 단에서 평가. 평가 지점이 다르면 타이밍/성능에 차이가 생긴다.
- selector 해석: 대부분 동일하나,
ipBlock의 예외(except) 처리 등 미묘한 부분에서 구현체 간 차이가 보고된 적 있다. 규격은 같아도 경계 케이스에서는 다를 수 있다.
그래서 정책을 넣기 전에 "내 CNI가 NetworkPolicy를 지원하는가" 확인이 필수고, 정확한 디버깅을 위해서는 "어떻게 평가하는가"까지 아는 것이 필요하다. Hubble(Cilium)/Calico 로그가 그 "어떻게"를 보여준다. (Kubernetes docs - NetworkPolicy 지원 CNI)
NetworkPolicy 평가 알고리즘 — 여러 정책이 모일 때
한 Pod에 여러 NetworkPolicy가 동시에 적용될 수 있다. 그때 "허용이냐 거부냐"를 어떻게 정할까? Kubernetes의 규칙은 단순하되 오해하기 쉽다:
flowchart TD
PKT["패킷 (src → 이 Pod)"] --> COLLECT{"이 Pod + 이 방향(ingress)<br/>선택하는 정책들을 모아라"}
COLLECT --> UNION["정책들의 허용 항목을<br/>합집합(OR)"]
UNION --> Q{"패킷이 합집합 안에<br/>들어가나?"}
Q -->|예| ALLOW["허용"]
Q -->|아니오| DENY["거부(기본 거부 전환)"]
핵심 규칙:
- 이 Pod의 해당 방향(ingress 또는 egress)을 선택하는 정책이 하나라도 있으면 → 그 방향은 화이트리스트 모드(명시된 것만 허용, 나머지 거부).
- 여러 정책이 있으면 그 허용 항목들의 합집합으로 평가. 정책 간 *충돌이 아니라 *합산. 정책 A가 "web에서"를 허용하고 정책 B가 "monitoring에서"를 허용하면, 둘 다 허용.
- 거부 규칙은 없다. 합집합에 안 들어가면 자동 거부.
이 알고리즘을 모르면 "정책을 두 개 넣었더니 하나만 되는 것 같다"는 혼란이 생긴다 — 사실은 둘이 합쳐져 더 많이 열린다. 방향이 반대다.
실제 다중 정책 시나리오 — 점진적 허용 누적
db Pod에 세 정책이 순차적으로 쌓였다고 하자:
| 정책 | 허용 |
|---|---|
allow-web |
web Pod → db:5432 |
allow-migration |
migration Job → db:5432 |
allow-monitoring |
prometheus → db:9187 (메트릭) |
결과: db의 ingress는 이 세 출처만 허용, 나머지 전부 거부. 세 정책의 합집합. 새 팀이 "db에 백업 도구도 접근하게 해 달라"고 하면 — 네 번째 정책을 추가(백업 도구 → db). 이렇게 정책이 누적되면서 허용 목록이 커진다.
이 누적 모델의 함정: 시간이 지나며 정책이 많아지면 "db에 결국 뭐가 접근하나?"가 불투명해진다. 정책이 합집합이므로 하나를 빼도 다른 것들이 여전히 열려 있을 수 있다. 주기적 정책 감사가 필요. Cilium Hubble이 "실제로 누가 접근하나"를 관측해 이 감사를 돕는다.
egress default-deny의 함정 — 외부 의존성이 조용히 막힌다
ingress만 default-deny 하고 egress는 안 한 경우가 흔하다. "들어오는 건 막되 나가는 건 자유롭게"라는 의도. 그런데 egress default-deny를 나중에 켜면 — 조용히 부서지는 것들이 생긴다:
- DNS(CoreDNS) 질의 — egress가 막히면 svc-name 풀이 실패 → Service 통신 전부 안 됨.
- 외부 API 호출 — 갑자기 타임아웃.
- 이미지 레지스트리 pulling(신규 Pod) — 막힘.
- 메트릭 스크랩 — prometheus가 못 긁음.
그래서 egress 정책을 짤 땐 필수 외부 의존성(DNS, 레지스트리, 외부 API)을 먼저 나열해 허용해야 한다. 특히 DNS(udp/53 to kube-dns)는 거의 모든 Pod가 필요 — 이것을 빼먹으면 "egress 켰더니 다 안 된다" 사태가 벌어진다.
# egress default-deny 시 필수 — DNS 허용
egress:
- to:
- namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}
podSelector: {matchLabels: {k8s-app: kube-dns}}
ports: [{protocol: UDP, port: 53}]
이것 없이 egress default-deny를 켜면 DNS가 막혀 Service 이름 해석이 안 되어 클라이언트가 "서비스가 사라졌다"고 보고한다. 흔한 사고.
직접 확인하기
# Kubernetes 1.36 — default-deny + 허용 정책 효과
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: default-deny, namespace: default}
spec: {podSelector: {}, policyTypes: [Ingress, Egress]}
EOF
# 이제 default namespace의 모든 Pod 트래픽이 막힘
kubectl run a --image=busybox:1.36 --rm -it --restart=Never -- \
wget -T2 -qO- <다른 Pod IP> 2>&1 || echo "BLOCKED"
확인할 것: default-deny 적용 후 wget이 실패(timeout). 단, kindnetd(기본 CNI)는 NetworkPolicy를 지원하지 않을 수 있음 — Calico/Cilium 클러스터에서 확인 권장(k8s-verify 스킬).
# 특정 허용 추가
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-web, namespace: default}
spec:
podSelector: {matchLabels: {app: db}}
policyTypes: [Ingress]
ingress:
- from: [{podSelector: {matchLabels: {app: web}}}]
ports: [{protocol: TCP, port: 5432}]
EOF
확인할 것: web→db만 열리고 나머지는 여전히 막힘.
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "Kubernetes는 기본이 보안이다" | 네트워크는 기본 전부 허용. 정책 넣어야 닫힘 |
| "정책은 허용과 거부 둘 다 있다" | 기본 NetworkPolicy는 허용만. deny 규칙은 CRD 정책 |
| "정책 넣으면 그 Pod만 영향" | 그 Pod의 선택된 방향 전체가 화이트리스트로. 나머지 막힘 |
| "NetworkPolicy는 항상 동작한다" | CNI가 지원해야. kindnetd 등은 미지원 가능 |
| "ingress와 egress는 같이 켠다" | 독립. 한쪽만 정책이면 다른 쪽은 전부 허용 |
| "정책에 막힌 트래픽은 에러가 난다" | 조용히 버려짐(로그 안 남음). Hubble 등으로 추적 |
| "여러 정책은 충돌한다" | 합집합. 정책이 많아질수록 더 많이 열림 |
요약 — 이 글의 결론
- Kubernetes 네트워크 기본은 "전부 허용" — 연결성 우선 철학. 보안은 사용자가 명시적으로 켜야. NetworkPolicy가 그 도구.
- 정책 하나가 화이트리스트 모드를 켠다. 어떤 Pod에 특정 방향 정책이 있으면, 그 방향은 명시된 것만 허용. 나머지는 거부 — "정책 넣었더니 다 막힘"의 원인.
- ingress와 egress는 독립. 한쪽만 정책이면 다른 쪽은 전부 허용. 양방향 격리 원하면 둘 다.
- 두 개의 podSelector가 역할이 다르다 — 최상위는 "통제 대상", from/to 안은 "허용할 상대". 이 분리가 정책 읽기의 열쇠.
- 여러 정책은 합집합 — 충돌이 아니라 누적. 정책이 많아지면 더 많이 열리므로 주기적 감사 필요.
- 기본 NetworkPolicy의 한계: deny 규칙 없음, namespace 간/글로벌 불편, L7 불가, 로그 안 남음. 이 한계를 Calico/Cilium CRD가 넘는다.
- 정책 동작은 CNI 구현체에 의존. NetworkPolicy 미지원 CNI(Flannel 기본 등)에선 정책이 무시. CNI 지원 확인이 전제.
- default-deny + whitelist가 표준 패턴 — 전부 거부로 시작하고 허용 항목을 추가. 단 egress 켤 땐 DNS(kube-dns:53)를 먼저 허용.
생각해 볼 문제
- NetworkPolicy를 하나도 안 넣은 namespace의 보안 상태는? "안전"이라고 주장할 수 있는가?
- 웹 Pod에 ingress 정책만 넣었다. egress(외부 API 호출)는 어떻게 되나?
- "특정 IP만 거부"를 기본 NetworkPolicy로 구현하려면? 왜 불편한가?
- kindnetd CNI에 NetworkPolicy를 넣었다. 효과는? 어떻게 확인하나?
- 정책에 막힌 트래픽을 추적하려 한다. 어떤 도구가 필요한가?
- default-deny를 켰다가 모니터링(Prometheus scrape)이 멈췄다. 어떻게 고치나?
- 한 Pod에 정책 3개가 적용 중이다. 셋 다 지우면 그 Pod의 트래픽은 어떻게 되나? (기본 상태로 복귀)
참고
- Kubernetes 공식 문서 - Network Policies - 접근 2026-07-13 (기본 허용, 화이트리스트)
- Kubernetes 공식 문서 - default-deny 정책 예 - 접근 2026-07-13
- Kubernetes 공식 문서 - NetworkPolicy 지원 CNI - 접근 2026-07-13
- Calico docs - NetworkPolicy vs Calico policy 비교 - 접근 2026-07-13
- Cilium docs - NetworkPolicy - 접근 2026-07-13
'Tech Artifacts > K8s Networking' 카테고리의 다른 글
| K8s Networking - 08. ingress (1) | 2026.07.14 |
|---|---|
| K8s Networking - 07. calico advanced-policy (0) | 2026.07.14 |
| K8s Networking - 05. cilium ebpf (0) | 2026.07.14 |
| K8s Networking - 04. calico (1) | 2026.07.14 |
| K8s Networking - 03. cni model (0) | 2026.07.14 |
