쿠버네티스 NetworkPolicy 설정 — Calico, Cilium, kube-router 비교

목차

기본 클러스터에서 파드 간 트래픽 차단율은 0%, 디폴트 deny 정책 한 줄을 적용하면 100%가 된다. 쿠버네티스 NetworkPolicy 설정은 이 두 극단 사이를 메우는 작업이다. 어떤 파드가 어떤 파드와 어떤 포트로 통신할지를 선언적으로 좁혀가는 일이다.

문제는 NetworkPolicy 리소스 자체는 단순한 스펙이고, 실제 차단을 누가 하느냐는 CNI 플러그인의 몫이라는 점이다. 같은 YAML을 적용해도 Calico, Cilium, kube-router가 각자 다르게 동작한다. 표현력이 다르고, 성능 오버헤드가 다르고, 디버깅 방법이 다르다.

침투 테스트 보고서에서 "한 파드가 RCE로 뚫리면 측면 이동(lateral movement)이 자유롭다"는 지적을 받은 뒤로 CNI 후보 셋을 놓고 한참 비교했다. 이번 글은 그 셋을 항목별로 따져본 기록이다.

배경 — 왜 NetworkPolicy가 필요한가

쿠버네티스의 기본 네트워크 모델은 "모든 파드가 모든 파드와 통신 가능"이다. 마이크로서비스 개발에는 편하지만 보안 관점에선 재앙이다. payment 서비스가 redis와 통신해야 한다고 해서 마케팅 cron job이 결제 DB에 접근할 이유는 없다.

따라서, NetworkPolicy는 이 평면 네트워크 위에 화이트리스트를 그린다. podSelector, namespaceSelector, ipBlock 세 가지 셀렉터로 ingress와 egress를 정의한다. 기본 동작은 "정책이 매칭되지 않으면 허용", 그래서 default deny 정책을 먼저 깔고 시작하는 게 표준이다.

# prod 네임스페이스의 모든 파드에 대해 ingress, egress 기본 차단
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: prod
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

그러나, 이걸 깔면 prod 네임스페이스의 모든 파드는 통신이 끊긴다. 여기에 화이트리스트 정책을 하나씩 추가하는 방식이다. 처음 도입할 때 kube-dns로 가는 53/UDP egress를 빼먹어서 30분간 클러스터가 마비된 적이 있다. 이 함정은 뒤에서 다시 다룬다.

비교 기준 — 무엇으로 평가했나

CNI 후보 셋을 동등하게 비교하려면 기준이 명확해야 한다. 회사 환경에서 의미가 있는 다섯 가지를 골랐다.

  • 표현력: 표준 NetworkPolicy 외에 L7(HTTP path, gRPC method) 정책이 가능한가
  • 성능: 정책 수가 늘어날 때 처리 지연과 CPU 사용률
  • 디버깅: "왜 차단됐는지" 추적 도구가 있는가
  • 러닝 커브: 팀원이 익히는 데 걸리는 시간
  • 생태계: 운영 도구, 모니터링 통합, 문서 품질

성능과 표현력은 트레이드오프 관계처럼 보였지만, 실제로는 그렇지 않았다. 오히려 디버깅 도구의 유무가 운영 부담을 좌우했다. 차단된 트래픽이 왜 차단됐는지를 빠르게 답해주지 못하면, 정책 한 줄 추가할 때마다 30분씩 시간이 새어 나간다.

항목별 비교 — Calico vs Cilium vs kube-router

세 후보의 핵심 특성을 정리하면 다음과 같다(2026년 4월 기준 안정 버전 — Calico v3.27, Cilium v1.15, kube-router v2.1).

항목 Calico Cilium kube-router
데이터플레인 iptables / eBPF 선택 eBPF (전면) iptables + ipset
L7 정책 별도 CRD (GlobalNetworkPolicy 일부) CiliumNetworkPolicy 기본 지원 미지원
표준 NetworkPolicy 지원 지원 지원
디버깅 도구 calicoctl + felix 로그 Hubble + UI 없음 (iptables 직접)
정책 적용 지연(체감) 수백 ms 수십 ms 수백 ms
도입 난이도

표만 보면 Cilium이 압도적으로 보이지만 실제로는 그렇게 단순하지 않다. Cilium은 커널 버전을 가린다. 5.4 이하 노드에서는 eBPF 기능이 제한되고, 일부 매니지드 노드 이미지는 eBPF 활용이 까다롭다.

Calico — 무난한 디폴트

또한, Calico는 OSS 영역에서 가장 오래됐고 문서가 두껍다. 표준 NetworkPolicy를 그대로 받고, 추가로 GlobalNetworkPolicy CRD를 통해 클러스터 전역 정책을 표현한다. iptables 모드와 eBPF 모드를 선택할 수 있고, 운영팀이 iptables에 익숙하다면 디버깅이 그나마 직관적이다.

# Calico GlobalNetworkPolicy — 모든 워크로드의 metadata 엔드포인트 egress 차단
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: deny-egress-to-cloud-metadata
spec:
  selector: all()
  types:
    - Egress
  egress:
    # AWS/GCP/Azure 공통 metadata IP 차단
    - action: Deny
      destination:
        nets:
          - 169.254.169.254/32
    - action: Allow

이 정책 한 줄로 SSRF로 인한 IMDSv1 토큰 탈취를 막는다. 침투 테스트 단골 지적사항인데, 표준 NetworkPolicy로는 GlobalNetworkPolicy 같은 클러스터 전역 표현이 안 된다. 네임스페이스마다 같은 정책을 복사해야 한다.

Cilium — eBPF로 다 한다

그러나, Cilium은 데이터플레인이 전부 eBPF다. iptables를 거의 거치지 않아 정책 수가 많을 때 성능이 안정적이고, L7 필터링(HTTP path, Kafka topic, gRPC 메서드)을 표준 기능으로 제공한다. 같이 따라오는 Hubble이라는 관측 컴포넌트가 핵심이다.

# Cilium L7 정책 — payment-api는 /api/v1/charge POST만 허용
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: payment-api-l7
  namespace: prod
spec:
  endpointSelector:
    matchLabels:
      app: payment-api
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: order-service
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              # 결제 생성 외 다른 경로는 전부 거부
              - method: "POST"
                path: "/api/v1/charge"

L4까지만 보는 표준 NetworkPolicy로는 "POST /api/v1/charge"는 허용하고 "DELETE /admin/users"는 차단하는 식의 표현이 불가능하다. Cilium은 이걸 한 리소스로 처리한다. 다만 L7 정책은 Envoy 프록시를 사이드카처럼 끼우는 방식이라 약간의 지연이 추가된다 — 측정해보면 호출당 1~3ms 수준이다.

kube-router — 단순함이 무기

결국, kube-router는 데몬셋 하나로 끝난다. CNI, kube-proxy 대체, NetworkPolicy를 한 바이너리에서 처리한다. iptables + ipset 조합이라 표현력은 표준 NetworkPolicy 수준이고 L7은 없다. 그 대신 풋프린트가 작다. RAM 50~80MB 수준에서 동작한다. 작은 클러스터, 엣지 환경에선 매력적이다.

특히, 문제는 디버깅 도구가 없다는 점이다. 차단된 트래픽의 원인을 찾으려면 노드에 들어가서 iptables -L -n -v로 카운터를 보거나 conntrack을 직접 뒤져야 한다. 정책 수가 100개 안쪽이면 견딜 만한데, 그 이상부터는 매번 노드 SSH 접속이 일이 된다.

벤치마크 — 정책 1,000개 환경

내부 스테이징 클러스터(노드 6대, c5.2xlarge, 1.29.x)에서 합성 정책 1,000개를 적용하고 측정했다. 측정값은 환경에 따라 달라질 수 있어 "이 환경에서 이 결과가 나왔다" 정도로 받아들이면 된다.

결국, :::stats

  • 정책 적용 지연(p99): Calico iptables 320ms / Calico eBPF 90ms / Cilium 45ms / kube-router 410ms
  • CPU 오버헤드(노드당): Calico iptables 8% / Cilium 3% / kube-router 11%
  • 메모리 풋프린트: Cilium 280MB / Calico 180MB / kube-router 70MB :::

이처럼, 체감상 정책이 수십~수백 개 수준이면 어느 쪽을 써도 차이가 거의 없다. 1,000개를 넘어가면 iptables 기반 솔루션의 동기화 시간이 눈에 띄게 늘어난다. 정책이 추가될 때마다 iptables 룰셋을 다시 컴파일하는 구조 때문이다. eBPF 기반은 맵 업데이트만 하면 돼서 거의 일정한 지연을 보인다.

따라서, 성능 차이가 의미 있게 벌어지는 임계는 대략 정책 500개 부근이었다. 그 아래에서는 어느 솔루션을 골라도 운영 체감이 비슷했다.

디버깅 — 가장 큰 차이는 여기서 갈린다

그런데, 운영하면서 가장 자주 받는 알림은 "내 파드에서 외부 API 호출이 갑자기 안 돼요"다. NetworkPolicy 변경 직후에 이런 일이 잘 터진다. 디버깅 경로가 CNI마다 완전히 다르다.

결국, Cilium에서는 Hubble CLI 한 줄로 끝난다.

# 최근 5분간 차단된 흐름만 필터링
hubble observe --verdict DROPPED --since 5m \
  --from-pod prod/payment-api-7c9d \
  -o compact

# 출력 예시
# Mar 26 12:03:11.221 prod/payment-api-7c9d -> 169.254.169.254:80
#   tcp-flags: SYN  policy-verdict:none DROPPED (Policy denied)

drop된 패킷의 source/destination, 어떤 정책에 매칭되어 막혔는지가 즉시 출력된다. Hubble UI는 서비스 맵 위에 끊긴 화살표를 빨갛게 표시해준다. 이걸 보고 나면 다른 도구로 돌아가기 힘들다.

게다가, Calico는 calicoctl로 정책을 조회하고 felix 로그를 뒤져야 한다. iptables 모드라면 노드에 들어가서 cali- 체인을 추적한다. 시행착오를 거쳐 익숙해지면 빠르지만 처음에는 헤맨 시간이 길다.

# Calico iptables 모드 디버깅 — 노드에서 직접 확인
sudo iptables -t filter -L cali-fw-cali12345 -n -v
# 카운터(pkts) 값이 늘어나는 체인을 추적해야 한다

즉, kube-router는 도구가 사실상 없다. iptables 카운터를 직접 보거나 tcpdump를 띄워야 한다. 정책 수가 많아지면 분석에 한 시간씩 걸리는 경우가 잦다.

도입 시 부딪히는 함정 — DNS와 metadata

CNI를 무엇을 고르든 default deny를 적용하는 순간 거의 100% 부딪히는 함정 두 개가 있다. 신규 도입 시 가장 먼저 점검할 항목이다.

그러나, 첫 번째는 DNS다. egress 차단을 켜면 kube-dns(또는 CoreDNS)로 가는 53/UDP가 막혀 모든 서비스 디스커버리가 실패한다. 정책에 kube-system 네임스페이스의 dns 파드 selector를 명시적으로 허용해야 한다.

# DNS만 별도로 열어주는 egress 정책
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: prod
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

그래서, 두 번째는 클라우드 metadata 서비스다. AWS, GCP, Azure 모두 169.254.169.254로 동일하다. SSRF 공격의 단골 표적이다. 명시적으로 차단하는 정책을 클러스터 도입 첫날 깔아두면 침투 테스트 보고서에서 한 줄을 줄일 수 있다.

그런데, 세 번째는 정책 변경 후 검증이다. 정책 한 줄을 추가하기 전에 dry-run으로 영향 범위를 확인할 수 있는 도구가 CNI마다 다르다. Cilium은 cilium policy trace, Calico는 calicoctl get policy --output yaml 후 시뮬레이션 도구. kube-router는 본질적으로 dry-run이 없어서 스테이징에서 충분히 굴려보고 prod에 올리는 절차가 필수다.

세 솔루션의 공통 권장 액션을 정리하면 도입 첫날에 default-deny + DNS 허용 + metadata 차단 세 정책을 같이 적용하고, 그 다음 서비스 단위로 화이트리스트를 좁혀가는 순서다. 이 순서를 어기면 prod 클러스터에서 DNS 장애로 시작하는 흔한 시나리오에 빠진다.

그래서, 참고 자료는 공식 문서 쪽이 가장 정확하다. Kubernetes NetworkPolicy 공식 문서 (2026-03 업데이트), Cilium v1.15 Release Notes, Calico v3.27 정책 가이드에서 시작하면 된다.

관련 글