"이 IP만 거부해라" — 기본 NetworkPolicy가 못 하는 것을 Calico CRD가 푸는 법

보안 팀이 한 가지를 더 요구했다: "특정 의심 IP에서 오는 모든 트래픽을 거부해라." 팀은 06장의 기본 NetworkPolicy로 시도했다. 그런데 기본 NetworkPolicy엔 거부(deny) 규칙이 없다 — 모든 규칙이 "허용"이다. "화이트리스트로 우회하라"는 답을 들었지만, IP 대역이 수천 개면 화이트리스트는 현실이 아니다. 팀은 Calico CRD 정책(GlobalNetworkPolicy)으로 넘어갔다 — 거부 규칙이 있고, L7까지, namespace마다 따로 안 써도 된다.

이 글이 푸는 것은: 06장에서 본 기본 NetworkPolicy의 네 가지 한계를 Calico CRD 정책이 어떻게 하나씩 넘는가다. Calico를 "CNI + 정책 엔진" 둘로 보는 시선이 핵심이다.

06장의 네 한계, Calico가 넘는 방식

06장 끝에서 기본 NetworkPolicy의 한계 네 가지를 짚었다: (1) 거부 규칙 없음, (2) namespace 간/글로벌 정책 불편, (3) L7 불가, (4) 로그 안 남음. Calico는 자체 CRD로 이 네 가지를 넘는다 — 거부 규칙, 글로벌 적용, L7, 그리고 순서 제어까지.

Calico가 제공하는 CRD 정책 두 종류: (Calico docs - NetworkPolicy)

  • GlobalNetworkPolicy: 클러스터 전체에 적용(namespace 독립).
  • NetworkPolicy(Calico CRD, crd.projectcalico.org/v1): K8s 기본과 비슷하지만 더 강력(deny/L7 등).

이 CRD들이 K8s 기본 NetworkPolicy(networking.k8s.io/v1)와 다른 점이 한계를 넘는 지점이다. 하나씩 보자.

한계 1 넘기 — 거부(deny) 규칙을 직접 쓴다

K8s 기본은 "허용" 규칙만 있다. Calico CRD는 거부 규칙을 직접 쓴다:

# Kubernetes 1.36, Calico CRD
apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata: {name: block-bad-ip}
spec:
  order: 10                       # 작을수록 먼저 평가
  ingress:
  - action: Deny                  # 거부!
    source:
      nets: [203.0.113.0/24]      # 의심 IP 대역
  - action: Pass                  # 다음 정책으로

action: Deny가 핵심이다. "이 IP만 거부하고 나머지는 다른 정책에 맡긴다"를 직접 표현한다. K8s 기본으로는 "이 IP 제외 전부 허용"을 화이트리스트로 짜야 하므로, IP가 많으면 불가능에 가깝다 — 도입의 팀이 부딪힌 벽이 바로 이것.

한계 2 넘기 — 글로벌 정책 (namespace마다 안 써도)

K8s 기본 NetworkPolicy는 namespace에 종속 — namespace마다 따로 만들어야 한다. namespace가 50개면 default-deny 정책도 50개. Calico GlobalNetworkPolicy클러스터 전체에 한 번에 적용된다. "모든 namespace의 모든 Pod에 default-deny"를 한 정책으로.

한계 3 넘기 — L7 (HTTP 경로/메서드) 제어

K8s 기본은 L3/L4(IP/포트)만. Calico(그리고 Cilium) CRD는 L7까지: (Calico docs - L7 HTTP 정책)

spec:
  ingress:
  - action: Allow
    source: {selector: "app == 'gateway'"}
    http:
      methods: ["GET"]            # GET만 허용
      paths: ["/api/.*"]          # /api/ 경로만

"gateway Pod만, 그것도 /api/에 GET으로" 같은 세밀한 HTTP 정책이 가능. 단 L7 기능은 Calico 버전/모드(felix)에 따라 지원 범위가 다름 — 실측 필요.

한계 4 넘기 — 순서(order) 제어

K8s 기본 정책들은 순서가 없다 — 모두 합쳐져 합집합으로 평가. Calico는 order 필드로 정책 평가 순서를 명시한다. "이 거부 정책을 먼저, 그 다음 허용"을 보장. 보안 정책의 평가 순서가 중요할 때 결정적이다.

두 정책 모델 비교 — K8s 기본 vs Calico CRD

K8s NetworkPolicy Calico CRD
규칙 종류 허용만 허용/거부/패스
적용 범위 namespace 내 namespace 또는 글로벌
레이어 L3/L4 L3/L4/L7
순서 없음(합집합) order 필드
로그 없음 로그/메트릭 가능

tier — 정책을 계층으로 묶어 평가 순서를 구조화

order가 개별 정책의 순서라면, tier는 정책 그룹*의 순서다. Calico는 정책을 tier로 묶어 평가 흐름을 구조화한다. 예: "보안 tier(거부 규칙) → 플랫폼 tier → 앱 tier". 각 tier가 순서대로 평가되며, 한 tier에서 Deny가 발동하면 *즉시 거부(이후 tier 안 봄), Allow면 즉시 허용, Pass면 다음 tier로 넘어간다. (Calico docs - Tiers)

flowchart TD
    PKT["패킷"] --> T1["tier: security (order 10)<br/>거부 규칙 (의심 IP, 알려진 공격)"]
    T1 -->|Deny| DROP["거부 (즉시, 이후 안 봄)"]
    T1 -->|Pass| T2["tier: platform (order 20)<br/>플랫폼 팀 정책 (메트릭 접근 등)"]
    T2 -->|Allow| OK1["허용 (즉시)"]
    T2 -->|Pass| T3["tier: app (order 30)<br/>앱 팀 정책 (서비스 간 통신)"]
    T3 -->|Allow/Deny| FINAL["최종 결정"]

이 구조가 주는 가치: 책임 분리 + 평가 순서 보장. 보안 팀은 security tier에서 "무조건 거부" 규칙을, 플랫폼 팀은 platform tier에서, 앱 팀은 app tier에서 각자 관리. 보안의 거부 규칙이 항상 먼저 평가되어, 앱 정책이 실수로 위험한 것을 열어도 보안 tier가 우선한다.

deny + allow의 전형적 패턴 — "이것만 빼고 전부 허용"

K8s 기본 NetworkPolicy는 deny 규칙이 없어 "이 IP만 거부"를 힘들게 했다. Calico CRD로 이 패턴이 자연스럽다:

# tier 안에서: 의심 IP 거부 → 그 외는 다음 정책으로
ingress:
- action: Deny
  source: {nets: ["203.0.113.0/24"]}    # 의심 대역 즉시 거부
- action: Pass                            # 나머지는 다음 규칙/정책으로

또는 "특정 Pod만 거부하고 나머지 허용":

- action: Deny
  source: {selector: "app == 'untrusted'"}
- action: Allow                            # untrusted 아닌 것은 허용

이 패턴이 K8s 기본 NetworkPolicy로는 불가능(허용만 가능)했던 것이다. Calico CRD의 deny 규칙이 이 "블랙리스트" 패턴을 자연스럽게 만든다.

tier 평가의 실전 예 — 세 tier를 거치는 패킷

구체적 시나리오로 tier 평가를 체감하자. 한 패킷이 web Pod에서 db Pod로 ingress:

패킷: web(10.0.1.5) → db(10.0.2.3):5432

tier 1: security (order 10)
  규칙: Deny source.nets=[203.0.113.0/24]   → web IP(10.0.1.5)가 아니므로 Pass
  결과: Pass → 다음 tier

tier 2: platform (order 20)
  규칙: Allow source.selector="app=='monitoring'" → web은 monitoring 아님
  규칙: Pass                                 → 다음 tier
  결과: Pass

tier 3: app (order 30)
  규칙: Allow source.selector="app=='web'" ports=[5432]   → web 맞음, 5432 맞음
  결과: Allow → 최종 허용

이 평가 흐름이 "보안 거부 → 플랫폼 → 앱 허용"의 3단계를 거치는 것을 보여준다. 만약 web이 의심 IP(203.0.113.x)였다면 tier 1에서 즉시 Deny — tier 2/3는 안 본다. 이 단락(short-circuit)이 보안 규칙을 항상 우선하게 만든다.

Felix가 정책을 커널 규칙으로 번역하는 과정

Calico 정책(CRD)은 YAML 선언이다. 그것이 실제로 패킷을 막으려면 커널(iptables/eBPF) 규칙으로 번역돼야 한다. Felix가 이 번역을 담당한다:

flowchart LR
    CRD["GlobalNetworkPolicy<br/>(YAML)"] --> FX["Felix (노드)"]
    FX --> COMPILE["정책 컴파일<br/>→ iptables 체인 또는 eBPF 프로그램"]
    COMPILE --> KERNEL["커널에 적용<br/>(실제 패킷 필터링)"]

Felix는 apiserver(또는 typha)에서 정책을 watch. 정책이 바뀌면 Felix가 그것을 컴파일해 커널 규칙으로 갱신. 이것이 "정책 선언 → 커널 실행"의 다리. iptables 모드면 iptables 체인으로, eBPF 모드면 eBPF 프로그램으로. 모드가 번역 대상을 결정.

이 컴파일이 "정책이 많을 때" 비용이 된다. 정책 수백 개를 iptables 체인 수천 줄로 번역하면 — 적용 시간/메모리가 커진다. eBPF 모드는 이 비용을 해시 맵으로 줄인다. 이것이 Calico에도 eBPF 데이터플레인 옵션이 있는 이유 — iptables 병목을 같은 방식으로 해결.

Cilium CRD도 같은 길

Cilium도 자체 CRD(CiliumNetworkPolicy)로 거부/L7/순서를 지원한다. (Cilium docs - CiliumNetworkPolicy) Calico와 철학이 같다 — "K8s 기본 NetworkPolicy로는 부족하니, CNI 벤더가 더 강력한 정책 CRD를 얹는다." CNI 선택이 정책 표현력에도 영향을 미치는 이유다.

데이터플레인과 정책 엔진의 독립

04장에서 Calico가 CNI 데이터플레인(BGP/VXLAN)을 다뤘다. 정책 엔진은 그 데이터플레인 위에서 동작 — felix가 정책을 iptables/eBPF 규칙으로 번역해 노드에 깐다. 즉 Calico는 "네트워크 모드 + 정책 엔진"이 독립적으로 선택·작동한다. 데이터플레인은 BGP, 정책은 CRD 고급 모드로 — 둘 다 쓸 수 있다. 이 독립성이 04장의 핵심이었고, 이 장의 전제다.

직접 확인하기 (Calico 클러스터 필요)

# Calico 설치 후 — GlobalNetworkPolicy 적용
kubectl apply -f - <<'EOF'
apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata: {name: default-deny-all}
spec:
  selector: all()
  ingress:
  - action: Deny
  egress:
  - action: Deny
EOF

확인할 것: 글로벌 default-deny. K8s 기본 정책으로는 namespace마다 따로 해야 할 것을 한 번에.

kubectl get gnp                    # GlobalNetworkPolicy 목록
calicoctl get wep -o wide          # 워크로드 엔드포인트 (calicoctl 설치 시)

Calico 클러스터 + calicoctl 필요. k8s-verify 스킬 참조. 미검증 환경은 노트.

정책 디버깅 — calicoctl로 평가 추적

Calico 정책이 "왜 이 패킷을 막았나/허용했나"를 디버깅:

calicoctl get policy --all-projects -o yaml    # 전체 정책 보기
# 패킷 플로우 로그(FlowLogs)로 실제 평가 추적
kubectl logs -n calico-system <typha> | grep -i flow

이 도구들이 "정책을 넣었는데 의도대로 안 된다"를 추적하게 해 준다. tier 순서, deny/allow 조합의 복잡성을 디버깅하는 수단.

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"Calico CRD는 K8s NetworkPolicy를 대체한다" 보강. K8s 기본 정책도 여전히 동작(Calico가 구현). CRD는 더 강력한 추가
"거부 규칙은 K8s 기본에도 있다" 없다. CRD(Calico/Cilium)에만
"글로벌 정책은 namespace 정책을 무시한다" order/tier로 조율. 둘 다 평가됨
"L7 정책은 어디서나 된다" Calico 버전/모드, Cilium 설정에 따라. 실측 필요
"정책 순서는 의미 없다" K8s 기본은 순서 없음(합집합). Calico order/tier는 의미 있음
"Cilium은 Calico보다 정책이 약하다" 비슷한 수준(CRD로 deny/L7 지원)

요약 — 이 글의 결론

  • Calico CRD 정책(GlobalNetworkPolicy/Calico NetworkPolicy)이 06장의 기본 NetworkPolicy 네 한계를 넘는다.
  • 거부 규칙(action: Deny) — "이 IP만 거부" 직접 표현. K8s 기본은 허용만.
  • 글로벌 적용(GlobalNetworkPolicy) — namespace마다 안 써도 클러스터 전체 한 번에.
  • L7 제어(HTTP 메서드/경로) — K8s 기본은 L3/L4만. 단 버전/모드에 따라 지원 범위 다름.
  • 순서 제어(order/tier) — 평가 순서 보장. K8s 기본은 합집합(순서 없음). tier로 "보안 거부 → 앱 허용" 보장.
  • Felix가 CRD 정책을 커널(iptables/eBPF) 규칙으로 번역. 정책이 많으면 컴파일 비용 → eBPF 모드로 완화.
  • Cilium CRD도 같은 길 — CNI 벤더가 "K8s 기본 NetworkPolicy로는 부족하니 더 강력한 CRD를 얹는" 공통 패턴. CNI 선택이 정책 표현력에 영향.
  • Calico는 데이터플레인과 정책 엔진이 독립 — BGP 모드 + CRD 고급 정책, 둘 다 선택 가능.

생각해 볼 문제

  1. K8s 기본 NetworkPolicy로 "IP A만 거부, 나머지 허용"을 구현하려면? 왜 현실적으로 어려운가?
  2. GlobalNetworkPolicy의 order가 중요한 시나리오를 설계하라.
  3. L7 정책(HTTP 경로)을 쓰려면 Calico/Cilium이 패킷을 어디까지 검사해야 하나? (성능 의미)
  4. CiliumNetworkPolicy와 Calico GlobalNetworkPolicy를 같이 쓰면 충돌하는가? (같은 클러스터에서)
  5. tier를 쓰면 "보안 정책(거부) → 앱 정책(허용)" 순서를 보장할 수 있다. 이것이 없을 때의 위험은?
  6. K8s 기본 NetworkPolicy만 써도 충분한 클러스터는 어떤 조건인가? (반대로 CRD가 필요한 조건은?)

참고

'Tech Artifacts > K8s Networking' 카테고리의 다른 글

K8s Networking - 09. gateway-api  (0) 2026.07.14
K8s Networking - 08. ingress  (1) 2026.07.14
K8s Networking - 06. network-policy  (0) 2026.07.14
K8s Networking - 05. cilium ebpf  (0) 2026.07.14
K8s Networking - 04. calico  (1) 2026.07.14