Kubernetes NetworkPolicy

왜 NetworkPolicy가 필요한가

Kubernetes 클러스터에서 기본적으로 모든 Pod는 다른 모든 Pod와 자유롭게 통신할 수 있습니다. 공식 문서에 따르면, “네임스페이스에 정책이 없으면 해당 네임스페이스의 Pod에 대한 모든 인그레스/이그레스 트래픽이 허용됩니다.” 이는 개발 편의성은 높지만, 보안 관점에서는 하나의 Pod가 침해되면 클러스터 전체로 수평 이동(lateral movement)이 가능하다는 뜻입니다.

NetworkPolicy는 OSI 3~4계층(IP/포트)에서 Pod 간 트래픽을 제어하는 Kubernetes 네이티브 리소스입니다. 이 글에서는 공식 문서를 기준으로 NetworkPolicy의 동작 원리, 셀렉터 조합의 미묘한 차이, 그리고 실무 시나리오별 정책 설계를 다룹니다.

3-Tier NetworkPolicy 트래픽 흐름 도식
3-Tier 구조에서 NetworkPolicy로 허용/차단되는 트래픽 흐름

1. 핵심 개념: 격리(Isolation)의 두 방향

공식 문서는 Pod 격리를 두 가지 독립적인 방향으로 정의합니다:

방향 기본 상태 격리 조건 격리 후 동작
Ingress (수신) 비격리 — 모든 인바운드 허용 해당 Pod를 선택하는 NetworkPolicy에 Ingress가 policyTypes에 포함 해당 정책의 ingress 규칙에 명시된 연결만 허용
Egress (송신) 비격리 — 모든 아웃바운드 허용 해당 Pod를 선택하는 NetworkPolicy에 Egress가 policyTypes에 포함 해당 정책의 egress 규칙에 명시된 연결만 허용

핵심 원칙 (공식 문서):

  • NetworkPolicy는 충돌하지 않습니다 — 여러 정책이 같은 Pod에 적용되면, 허용 규칙의 합집합(union)이 최종 결과입니다.
  • 연결이 성립하려면 출발지의 egress 정책과 목적지의 ingress 정책 모두가 해당 연결을 허용해야 합니다.
  • Pod와 자신이 실행되는 노드 간 트래픽은 항상 허용됩니다 (IP에 관계없이).
  • 허용된 연결의 응답 트래픽(reply traffic)은 암시적으로 허용됩니다.

2. 셀렉터 조합의 함정: AND vs OR

공식 문서가 특별히 강조하는 부분이 from/to 배열 내 셀렉터 조합입니다. YAML의 하이픈 하나 차이로 의미가 완전히 바뀝니다.

패턴 A: 하나의 from 요소에 두 셀렉터 (AND)

ingress:
- from:
  - namespaceSelector:
      matchLabels:
        project: myproject
    podSelector:          # namespaceSelector와 같은 레벨 → AND
      matchLabels:
        role: frontend

의미: project=myproject 네임스페이스 안에서 role=frontend 레이블을 가진 Pod만 허용.

패턴 B: 두 개의 from 요소 (OR)

ingress:
- from:
  - namespaceSelector:
      matchLabels:
        project: myproject
  - podSelector:          # 별도 배열 요소 → OR
      matchLabels:
        role: frontend

의미: project=myproject 네임스페이스의 모든 Pod 또는 같은 네임스페이스에서 role=frontend Pod. 전자가 훨씬 넓은 범위를 허용합니다.

공식 문서 권고: “확실하지 않으면 kubectl describe로 Kubernetes가 정책을 어떻게 해석했는지 확인하세요.”

패턴 YAML 구조 의미 허용 범위
AND 같은 - 아래에 두 셀렉터 두 조건을 동시에 만족 좁음 (특정 NS의 특정 Pod)
OR 각각 별도 - 둘 중 하나만 만족해도 허용 넓음 (NS 전체 + 로컬 Pod)

3. Default 정책: Zero-Trust 네트워크의 시작점

공식 문서는 네임스페이스별 기본 정책 4가지 패턴을 제시합니다. 실무에서 가장 많이 사용하는 조합을 시나리오별로 정리합니다.

패턴 1: 모든 트래픽 차단 (Default Deny All)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}        # 네임스페이스의 모든 Pod 선택
  policyTypes:
  - Ingress
  - Egress

이 정책을 적용하면 해당 네임스페이스의 모든 Pod가 즉시 격리됩니다. 이후 필요한 통신만 개별 NetworkPolicy로 허용하는 것이 Zero-Trust 접근입니다.

주의: Default Deny Egress를 적용하면 DNS 조회도 차단됩니다. CoreDNS에 대한 egress를 반드시 허용해야 합니다.

패턴 2: DNS 허용 정책 (Default Deny와 항상 함께)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
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

4. 실전 시나리오: 3-Tier 애플리케이션 보안

Frontend → Backend → Database 구조를 NetworkPolicy로 보호하는 전체 예시입니다.

네임스페이스/레이블 구조

# 네임스페이스: production
# Pod 레이블:
#   tier: frontend  (Nginx/React)
#   tier: backend   (Spring Boot/NestJS)
#   tier: database  (MySQL)

정책 1: Database — Backend에서만 3306 접근 허용

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-allow-backend-only
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: database
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: backend
    ports:
    - protocol: TCP
      port: 3306

정책 2: Backend — Frontend에서 8080, Database로 3306 허용

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: frontend
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          tier: database
    ports:
    - protocol: TCP
      port: 3306
  - to:                    # DNS 허용
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53

정책 3: Frontend — 외부 Ingress 허용, Backend로만 Egress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: frontend
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - ports:                 # 모든 소스에서 80/443 허용 (Ingress Controller)
    - protocol: TCP
      port: 80
    - protocol: TCP
      port: 443
  egress:
  - to:
    - podSelector:
        matchLabels:
          tier: backend
    ports:
    - protocol: TCP
      port: 8080
  - to:                    # DNS 허용
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53

5. 전제 조건과 제약사항

공식 문서가 명시한 중요한 제약사항들:

항목 내용
CNI 플러그인 필수 NetworkPolicy는 네트워크 플러그인이 구현합니다. 지원하지 않는 CNI(예: Flannel 기본 모드)에서는 리소스를 생성해도 아무 효과가 없습니다.
지원 CNI 예시 Calico, Cilium, Weave Net, Antrea 등
L4만 제어 TCP, UDP, SCTP만 보장됩니다. ICMP 등 다른 프로토콜은 CNI에 따라 동작이 다릅니다.
포트 범위 endPort 필드로 범위 지정 가능 (v1.25 stable). endPortport 이상이어야 합니다.
ipBlock과 SNAT 클러스터 외부 IP 기반 필터링은 LoadBalancer/NodePort의 SNAT에 영향받습니다. 실제 소스 IP가 보존되는지 확인 필요합니다.
노드 트래픽 Pod가 실행되는 노드와의 트래픽은 NetworkPolicy와 무관하게 항상 허용됩니다.

6. 운영 체크리스트: NetworkPolicy 도입 절차

# 단계 설명
1 CNI 확인 kubectl get pods -n kube-system에서 Calico/Cilium 등 확인. Flannel만 있으면 NetworkPolicy 미지원.
2 Staging 먼저 적용 운영 환경에 바로 적용하지 마세요. Default Deny가 DNS를 차단하면 모든 서비스가 동시에 실패합니다.
3 Default Deny + DNS 허용을 세트로 반드시 DNS 허용 정책을 먼저 적용한 후 Default Deny를 적용합니다.
4 필요한 통신만 개별 허용 각 tier/서비스별로 최소 권한 원칙에 따라 ingress/egress를 허용합니다.
5 kubectl describe netpol로 검증 YAML AND/OR 실수를 즉시 탐지할 수 있습니다.
6 통신 테스트 kubectl exec로 Pod 간 curl/nc 테스트. 차단된 연결은 timeout으로 나타납니다.
7 모니터링 연동 Cilium Hubble이나 Calico의 Flow Logs로 차단된 트래픽을 가시화합니다.

7. 자주 발생하는 실수와 디버깅

실수 1: Default Deny 후 DNS 허용을 빠뜨림

증상: Pod 내부에서 서비스 이름 해석 실패, curl: Could not resolve host

원인: Egress Default Deny가 CoreDNS(UDP 53)도 차단

해결: kube-system 네임스페이스의 kube-dns Pod에 대한 egress를 명시적으로 허용

실수 2: 셀렉터 AND/OR 혼동

증상: 특정 네임스페이스의 특정 Pod만 허용하려 했는데, 같은 네임스페이스의 모든 Pod가 접근 가능

원인: YAML에서 namespaceSelectorpodSelector를 별도 배열 요소로 작성 (OR)

해결: 같은 - 아래에 두 셀렉터를 배치 (AND)

실수 3: CNI가 NetworkPolicy를 지원하지 않음

증상: NetworkPolicy를 적용했는데 트래픽이 전혀 차단되지 않음

원인: Flannel 기본 모드 등 NetworkPolicy 미지원 CNI

해결: Calico 또는 Cilium으로 교체. k3s의 경우 --flannel-backend=none으로 Flannel을 비활성화하고 Calico를 별도 설치

# 디버깅: Pod 간 연결 테스트
kubectl exec -it frontend-pod -- curl -m 5 http://backend-svc:8080/health
# 차단된 경우 → timeout (5초 후 실패)

# 정책 해석 확인
kubectl describe networkpolicy backend-policy -n production

마무리

NetworkPolicy는 Kubernetes 클러스터의 보안 경계를 정의하는 핵심 리소스입니다. 기본 상태인 “모든 트래픽 허용”에서 벗어나 Default Deny로 시작하고, DNS 허용을 반드시 포함하며, 셀렉터 조합의 AND/OR 차이를 정확히 이해하면 실무에서 Zero-Trust 네트워크를 구현할 수 있습니다. 단, CNI 플러그인이 NetworkPolicy를 지원하는지 반드시 먼저 확인하세요.

참고 자료

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux