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엔 한계가 있다:

  1. 거부(deny) 규칙이 없다. 모든 규칙은 "허용". "이 IP만 거부"를 직접 못 쓴다(화이트리스트로 우회해야).
  2. namespace 간 정책이 불편. namespaceSelector로 가능하지만, "전체 클러스터에 적용"은 어렵다(namespace마다 따로).
  3. L7(HTTP 경로, 메서드) 불가. L3/L4(IP, 포트)만.
  4. 로그가 안 남는다. 정책에 막힌 패킷이 조용히 버려진다.

이 한계를 넘는 것이 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["거부(기본 거부 전환)"]

핵심 규칙:

  1. 이 Pod의 해당 방향(ingress 또는 egress)을 선택하는 정책이 하나라도 있으면 → 그 방향은 화이트리스트 모드(명시된 것만 허용, 나머지 거부).
  2. 여러 정책이 있으면 그 허용 항목들의 합집합으로 평가. 정책 간 *충돌이 아니라 *합산. 정책 A가 "web에서"를 허용하고 정책 B가 "monitoring에서"를 허용하면, 둘 다 허용.
  3. 거부 규칙은 없다. 합집합에 안 들어가면 자동 거부.

이 알고리즘을 모르면 "정책을 두 개 넣었더니 하나만 되는 것 같다"는 혼란이 생긴다 — 사실은 둘이 합쳐져 더 많이 열린다. 방향이 반대다.

실제 다중 정책 시나리오 — 점진적 허용 누적

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)를 먼저 허용.

생각해 볼 문제

  1. NetworkPolicy를 하나도 안 넣은 namespace의 보안 상태는? "안전"이라고 주장할 수 있는가?
  2. 웹 Pod에 ingress 정책만 넣었다. egress(외부 API 호출)는 어떻게 되나?
  3. "특정 IP만 거부"를 기본 NetworkPolicy로 구현하려면? 왜 불편한가?
  4. kindnetd CNI에 NetworkPolicy를 넣었다. 효과는? 어떻게 확인하나?
  5. 정책에 막힌 트래픽을 추적하려 한다. 어떤 도구가 필요한가?
  6. default-deny를 켰다가 모니터링(Prometheus scrape)이 멈췄다. 어떻게 고치나?
  7. 한 Pod에 정책 3개가 적용 중이다. 셋 다 지우면 그 Pod의 트래픽은 어떻게 되나? (기본 상태로 복귀)

참고

'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