기본 상태: 모든 파드는 서로 통신할 수 있다

쿠버네티스 클러스터를 처음 구성하면 네임스페이스와 무관하게 모든 파드가 서로 자유롭게 통신할 수 있는 상태다. 개발 단계에서는 편하지만, 운영 환경에서는 심각한 문제가 된다. 프론트엔드 파드가 탈취되면 공격자는 같은 클러스터의 데이터베이스, 내부 API, 시크릿을 다루는 배치 잡까지 횡방향으로 이동할 수 있다. 방화벽은 클러스터 경계만 막을 뿐, 파드 사이의 East-West 트래픽에는 손을 대지 못한다.

네트워크 폴리시(NetworkPolicy)는 이 East-West 트래픽을 파드 레이블 단위로 제어하는 쿠버네티스 표준 리소스다. IP나 노드가 아니라 레이블을 기준으로 규칙을 쓰기 때문에 파드가 스케일 아웃되거나 재스케줄링되어도 규칙이 그대로 유효하다.

동작 원리와 전제 조건

가장 먼저 알아야 할 점은 네트워크 폴리시가 CNI 플러그인에 의해 강제된다는 것이다. Calico, Cilium, Antrea 같은 폴리시 지원 CNI가 없으면 리소스를 apply해도 아무 효과가 없다. flannel 기본 구성처럼 폴리시를 지원하지 않는 환경에서는 규칙이 조용히 무시되므로, 적용 전 CNI가 폴리시를 강제하는지 반드시 확인해야 한다.

두 번째 핵심은 화이트리스트 모델이라는 점이다. 어떤 파드에 폴리시가 하나라도 걸리는 순간, 명시적으로 허용한 트래픽 외에는 전부 차단된다. 반대로 아무 폴리시도 없는 파드는 계속 전면 허용 상태다.

기본 차단(default deny)부터 시작한다

실무에서는 "필요한 것만 여는" 방향이 안전하다. 네임스페이스 단위로 인그레스를 전부 막는 폴리시를 먼저 깔고, 필요한 통신을 하나씩 여는 순서로 간다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}        # 네임스페이스 내 모든 파드 대상
  policyTypes:
    - Ingress            # egress는 열어둠

podSelector: {}는 해당 네임스페이스의 모든 파드를 선택한다. ingress 규칙을 비워 두었으므로 들어오는 트래픽은 전부 거부된다.

필요한 통신만 명시적으로 허용한다

이제 app: api 파드가 app: web 파드로부터 오는 8080 포트만 받도록 허용해 보자.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-to-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: web
      ports:
        - protocol: TCP
          port: 8080

여러 폴리시가 같은 파드에 걸리면 규칙은 OR로 합쳐진다. 즉 default-deny와 이 폴리시가 함께 있을 때, web에서 온 8080만 통과하고 나머지는 여전히 막힌다.

from/to 셀렉터의 의미 차이에 주의한다

가장 실수가 잦은 부분이 셀렉터 조합이다. 아래 두 표현은 완전히 다른 대상을 가리킨다.

표현의미
podSelector + namespaceSelector (한 항목)지정한 네임스페이스 안의 특정 파드
podSelector, namespaceSelector (별도 항목)어떤 네임스페이스든 조건 파드 OR 해당 네임스페이스 전체

YAML에서 하이픈(-) 위치 하나로 AND가 OR로 바뀐다. 두 셀렉터를 같은 리스트 항목에 두면 AND, 각각 별도 항목으로 두면 OR다. 크로스 네임스페이스 규칙을 쓸 때는 이 구분을 반드시 검증해야 한다.

egress와 DNS 함정

egress까지 default-deny로 잠그면 흔히 애플리케이션이 통신 불능에 빠진다. 원인은 대개 DNS다. 파드가 서비스 이름을 해석하려면 kube-dns(CoreDNS)로 향하는 UDP/TCP 53 포트가 열려 있어야 한다. egress를 막을 때는 이 예외를 반드시 함께 열어야 한다.

  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

검증과 운영상 유의점

폴리시를 적용한 뒤에는 실제 트래픽으로 검증한다. 임시 파드를 띄워 허용/차단 양쪽을 모두 확인하는 것이 안전하다.

# 허용되어야 하는 경로 (web 레이블 파드에서 실행)
kubectl -n production exec deploy/web -- curl -m 3 api:8080/health

# 차단되어야 하는 경로 (임시 파드에서 실행 → timeout이 정상)
kubectl -n production run test --rm -it --image=curlimages/curl \
  --restart=Never -- curl -m 3 api:8080/health

마지막으로 몇 가지 주의점이다. 네트워크 폴리시는 L3/L4까지만 다루므로 HTTP 경로나 메서드 단위 제어는 서비스 메시 영역이다. 또한 폴리시는 상태 기반(stateful)이라 허용된 연결의 응답 트래픽은 반대 방향 규칙 없이도 통과한다. 그리고 apply 순서에 관계없이 규칙은 누적 평가되므로, 운영에서는 default-deny를 먼저 배포한 뒤 허용 규칙을 붙이는 절차를 CI에 고정해 두는 편이 사고를 줄인다.