Kubernetes 멀티클러스터 네트워크 정책 설정 및 트래픽 격리 완벽 가이드

Kubernetes 멀티클러스터 네트워크 정책 설정 및 트래픽 격리 완벽 가이드

멀티클러스터 환경에서 Kubernetes 멀티클러스터 네트워크 정책 설정이 생각보다 복잡하다는 사실을, 처음 Cilium ClusterMesh를 실제 프로덕션에 도입했을 때 뼈저리게 실감했습니다. 클러스터 간 통신은 뚫렸는데, "어느 파드가 어느 클러스터의 어떤 서비스와 실제로 대화하고 있는가?"를 추적하는 일이 생각보다 훨씬 어려웠습니다. 게다가 규제 업종에서 요구하는 트래픽 격리 증빙까지 요구받으면, 단순한 NetworkPolicy YAML 몇 장으로는 턱없이 부족합니다.

CNI 선택이나 ClusterMesh 기본 설정에 관해서는 Kubernetes 멀티클러스터 네트워크 정책 완전 가이드에서 이미 다뤘습니다. 이 글은 그 다음 단계, 즉 Egress Gateway를 통한 트래픽 격리 아키텍처 설계, 네임스페이스 단위 Zero Trust 체계 수립, CI/CD 파이프라인에서의 네트워크 정책 자동 검증에 집중합니다. 보안 팀이 만족할 수 있는 수준의 네트워크 세그멘테이션과, 개발팀이 실수로 정책을 깨뜨리지 못하게 막는 가드레일까지, 단계별로 짚어보겠습니다.

Kubernetes 멀티클러스터 트래픽 격리가 어려운 이유

멀티클러스터 트래픽 격리의 핵심 난점은, 단일 클러스터의 NetworkPolicy가 클러스터 경계를 넘는 트래픽에 원칙적으로 적용되지 않는다는 구조적 한계에 있습니다.

Kubernetes 표준 NetworkPolicy는 파드 셀렉터와 네임스페이스 셀렉터를 기반으로 동작합니다. 하지만 클러스터 A의 파드가 클러스터 B의 서비스에 요청을 보낼 때, 그 패킷은 클러스터 A의 CNI 정책을 빠져나가는 순간부터 네이티브 Kubernetes 정책의 통제 밖에 놓입니다. 서비스 메시(Istio, Linkerd)를 쓰더라도 사이드카 프록시의 인증서 신뢰 체인이 두 클러스터에 걸쳐 올바르게 구성되지 않으면, mTLS가 클러스터 경계에서 실질적으로 무력화됩니다.

또 다른 문제는 소스 IP 불투명성입니다. 클러스터 간 트래픽이 NodePort나 LoadBalancer를 거치면, 수신 측 클러스터에서는 원래 파드의 IP 대신 노드 IP나 로드 밸런서 IP가 소스로 찍힙니다. 이렇게 되면 NetworkPolicy의 podSelector 기반 규칙이 무의미해지고, 공격자가 내부 노드 IP를 흉내 내면 정책을 우회할 수 있는 여지가 생깁니다.

실제로 금융권 프로젝트에서 PCI-DSS 심사를 준비하며 클러스터 간 트래픽 흐름을 문서화해야 했는데, 표준 NetworkPolicy만으로는 심사관이 요구하는 "격리 증빙"을 만들 수 없었습니다. Egress Gateway와 서비스 메시 정책을 계층적으로 구성하고 나서야 비로소 트래픽 경로를 감사 가능한 형태로 통제할 수 있었습니다.

격리 계층 적용 범위 한계
Kubernetes NetworkPolicy 파드 ↔ 파드 (동일 클러스터) 클러스터 간 트래픽 미지원
Istio AuthorizationPolicy 서비스 메시 내 서비스 간 메시 외부 트래픽 불가
Egress Gateway 외부/클러스터 간 트래픽 출구 통제 인그레스 측 별도 구성 필요
CNI 클러스터 메시 정책 멀티클러스터 전반 CNI 종속, 벤더 락인 위험

Egress Gateway를 활용한 클러스터 간 트래픽 제어 아키텍처

Egress Gateway는 클러스터에서 나가는 모든 외부 트래픽을 단일 출구 노드(또는 노드 그룹)로 집중시켜 소스 IP를 고정하고 트래픽을 감사 가능한 경로로 강제하는 아키텍처 패턴입니다.

Cilium의 CiliumEgressGatewayPolicy를 예로 들면, 특정 네임스페이스의 파드가 외부로 나가는 트래픽을 지정한 Egress 노드를 통해서만 흘려보낼 수 있습니다. 이렇게 하면 수신 측에서 항상 동일한 IP 대역에서 트래픽이 들어오므로, IP 기반 방화벽 규칙을 클러스터 경계에 일관되게 적용할 수 있습니다.

아래 다이어그램은 두 클러스터 간 Egress Gateway 기반 트래픽 흐름을 보여줍니다.

graph LR
  subgraph cluster_a["클러스터 A (us-east-1)"]
    pa1["payments 파드\n(namespace: payments)"]
    pa2["orders 파드\n(namespace: orders)"]
    eg["Egress Gateway\n노드 (고정 IP: 10.0.1.100)"]
    pa1 -->|CiliumEgressGatewayPolicy| eg
    pa2 -->|차단 - 정책 미적용| eg
  end

  subgraph cluster_b["클러스터 B (us-west-2)"]
    fw["인그레스 방화벽\n(허용: 10.0.1.100만)"]
    svc_b["inventory 서비스\n(namespace: inventory)"]
    fw --> svc_b
  end

  eg -->|고정 소스 IP| fw

핵심은 파드별 또는 네임스페이스별 Egress 정책을 명시적으로 정의하지 않은 트래픽은 클러스터 외부로 나가지 못하게 막는 것입니다. Cilium에서는 CiliumClusterwideNetworkPolicy의 Default Deny와 결합하면 이 목표를 달성할 수 있습니다.

# Egress Gateway 정책 예시 — payments 네임스페이스의 클러스터 B 접근만 허용
apiVersion: cilium.io/v2
kind: CiliumEgressGatewayPolicy
metadata:
  name: payments-to-cluster-b
spec:
  selectors:
    - podSelector:
        matchLabels:
          app: payment-processor
      namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: payments
  destinationCIDRs:
    # 클러스터 B의 서비스 CIDR — 수신 클러스터와 사전 합의된 범위
    - "172.20.0.0/16"
  egressGateway:
    nodeSelector:
      matchLabels:
        # egress-gateway=true 레이블이 붙은 전용 노드를 출구로 지정
        egress-gateway: "true"
    egressIP: "10.0.1.100"   # 수신 클러스터 방화벽에 등록할 고정 소스 IP

주의: Egress Gateway 노드는 반드시 별도 관리 그룹(Node Group)으로 분리하고, 해당 노드에 일반 애플리케이션 파드가 스케줄되지 않도록 taint를 설정해야 합니다. Egress 노드에 범용 워크로드가 올라가면 소스 IP 통제가 무너집니다.

Istio 환경이라면 EgressGateway 리소스와 VirtualService/DestinationRule을 조합해 동일한 패턴을 구현할 수 있습니다. Istio 공식 문서의 Egress Gateway 가이드는 이 구성을 상세히 다루며, 특히 ServiceEntry를 활용해 외부 엔드포인트를 메시 내에 등록하는 방식을 권장합니다.

네임스페이스 단위 Zero Trust 격리 설계 — Default Deny부터 시작하기

Zero Trust 네트워크 격리의 출발점은 "명시적으로 허용하지 않은 트래픽은 모두 차단" 하는 Default Deny 정책입니다. 단일 클러스터에서도 이 원칙을 지키기 어려운데, 멀티클러스터 환경에서는 이를 일관되게 적용하는 것이 더욱 까다롭습니다.

네임스페이스 단위 격리는 다음 세 계층을 순서대로 쌓아 올리는 방식으로 설계합니다.

1단계: 클러스터 전역 Default Deny

# Cilium ClusterwideNetworkPolicy — 모든 네임스페이스에 적용되는 Default Deny
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: default-deny-all
spec:
  endpointSelector: {}   # 빈 셀렉터 = 모든 파드에 적용
  ingress:
    - {}                 # Ingress 명시 없음 = 전체 차단
  egress:
    - toEndpoints:
        - matchLabels:
            "k8s:io.kubernetes.pod.namespace": kube-system  # DNS(CoreDNS) 허용 필수
      toPorts:
        - ports:
            - port: "53"
              protocol: ANY

2단계: 네임스페이스 간 허용 정책

# payments 네임스페이스가 orders 네임스페이스의 서비스만 호출 가능
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-payments-to-orders
  namespace: payments
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: orders
      ports:
        - protocol: TCP
          port: 8080
    # kube-dns 허용 (클러스터 내부 DNS 조회)
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53

3단계: 클러스터 간 인증 — mTLS 강제

Istio PeerAuthentication으로 네임스페이스 단위 mTLS를 강제합니다. STRICT 모드에서는 사이드카 없는 파드나 인증서 없는 외부 클라이언트의 요청이 자동 차단됩니다.

# payments 네임스페이스: mTLS STRICT 강제
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: payments
spec:
  mtls:
    mode: STRICT
---
# payments → orders 서비스 호출 권한 부여
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-payments
  namespace: orders
spec:
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              # 클러스터 A의 payments 서비스 어카운트만 허용
              - "cluster.local/ns/payments/sa/payment-processor"
      to:
        - operation:
            methods: ["GET", "POST"]
            paths: ["/api/v1/orders*"]

이 세 계층을 모두 적용하면, 클러스터 A의 payments 파드가 클러스터 B의 orders 서비스를 호출할 때 NetworkPolicy(Egress 허용) → Egress Gateway(소스 IP 고정) → mTLS(클러스터 간 인증) → AuthorizationPolicy(메서드·경로 단위 인가) 의 네 단계 검문을 통과해야 합니다. 이 체계가 실질적인 클러스터 간 Zero Trust의 골격입니다.

서비스 메시(Istio)와 CNI NetworkPolicy의 역할 분리 전략

"Istio가 있으면 NetworkPolicy는 필요 없지 않나요?"라는 질문을 현장에서 자주 받습니다. 두 도구는 역할이 명확히 다르므로 함께 써야 합니다.

항목 CNI NetworkPolicy (L3/L4) Istio AuthorizationPolicy (L7)
작동 계층 IP/포트 수준 HTTP/gRPC 메서드·헤더·경로 수준
사이드카 필요 여부 불필요 필요 (Envoy 사이드카)
성능 오버헤드 매우 낮음 (eBPF 기반) 보통 (프록시 홉 추가)
클러스터 간 적용 Cilium ClusterMesh로 확장 가능 Istio 멀티클러스터 메시 필요
적합한 시나리오 네트워크 세그멘테이션, 포트 봉쇄 API 수준 인가, JWT 검증, 속도 제한

실전에서 검증된 역할 분리 원칙은 다음과 같습니다.

CNI NetworkPolicy가 담당해야 할 것. - 네임스페이스 간 L3/L4 Default Deny 및 화이트리스트 - 클러스터 외부로의 Egress 차단 (Egress Gateway 정책 포함) - 데이터베이스·메시지 브로커 등 인프라 컴포넌트에 대한 포트 수준 접근 제어

Istio(서비스 메시)가 담당해야 할 것. - 서비스 어카운트 기반 클러스터 간 인증(mTLS + SPIFFE/SVID) - HTTP 메서드·경로·헤더 단위 인가 - 관찰 가능성(Kiali, Jaeger 트레이싱) 및 트래픽 관리(카나리, 재시도)

Cilium의 경우 L7 정책(toPorts.rules.http)을 CNI 레벨에서 직접 처리할 수 있어, 사이드카 없이도 HTTP 메서드 수준 제어가 가능합니다. 하지만 JWT 검증이나 멀티클러스터 서비스 어카운트 신뢰 같은 고급 인증 기능은 여전히 Istio 또는 별도 서비스 메시가 필요합니다. 두 도구를 중복으로 쓰는 영역은 최소화하고, 각자의 강점에 집중하도록 역할을 구분하는 것이 운영 복잡도를 낮추는 지름길입니다.

네트워크 정책 자동 검증 — CI/CD 파이프라인에 통합하기

네트워크 정책은 작성만큼이나 검증과 지속적인 감시가 중요합니다. 정책 파일이 Git에 병합되는 순간 즉시 효과가 발생하는데, 사람이 매번 수동으로 검토하면 실수를 막기 어렵습니다. 실제로 Default Deny 정책에 DNS 허용 규칙을 누락해 클러스터 전체가 DNS 조회를 못 하게 된 상황을 경험한 적이 있습니다. 그 이후로 CI 단계에서 정책 검증을 의무화했습니다.

CI/CD 파이프라인에 통합하는 세 가지 도구.

netpol-analyzer (Cilium 제공): NetworkPolicy YAML을 파싱해 파드 간 통신 가능 여부를 시뮬레이션합니다. 실제 클러스터 없이도 "A 파드가 B 파드와 통신할 수 있는가?"를 검증할 수 있습니다.

kube-networkpolicysimulator: 기존 클러스터의 파드 레이블을 기반으로 정책 변경이 어떤 영향을 미칠지 미리 계산해 줍니다.

conftest + OPA(Open Policy Agent): 네트워크 정책 YAML이 팀의 보안 기준을 준수하는지 Rego 정책으로 검사합니다. 예를 들어 "Default Deny 없는 네임스페이스가 있으면 실패"나 "포트 22 Ingress가 있으면 실패" 같은 규칙을 코드로 정의하고 PR 단계에서 강제할 수 있습니다.

아래는 GitHub Actions에서 conftest로 네트워크 정책 보안 기준을 검사하는 예시입니다.

# .github/workflows/netpol-validate.yml
name: Network Policy Validation

on:
  pull_request:
    paths:
      - "k8s/network-policies/**"

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install conftest
        run: |
          wget https://github.com/open-policy-agent/conftest/releases/download/v0.51.0/conftest_0.51.0_Linux_x86_64.tar.gz
          tar xzf conftest_0.51.0_Linux_x86_64.tar.gz
          sudo mv conftest /usr/local/bin/

      - name: Validate NetworkPolicy files
        run: |
          # policy/ 디렉토리의 Rego 정책으로 k8s/ 의 YAML을 검사
          conftest test k8s/network-policies/ \
            --policy policy/netpol-rules.rego \
            --all-namespaces

      - name: Simulate policy impact (netpol-analyzer)
        run: |
          # netpol-analyzer로 파드 간 연결 가능성 검증
          docker run --rm \
            -v $(pwd)/k8s:/k8s \
            ghcr.io/np-guard/netpol-analyzer:latest \
            list --dirpath /k8s \
            --fail-on changed-connections
# policy/netpol-rules.rego — 네트워크 정책 보안 기준 검사 규칙
package main

# Default Deny Ingress가 없는 네임스페이스 탐지
deny[msg] {
  input.kind == "NetworkPolicy"
  not input.spec.podSelector == {}
  msg := sprintf("NetworkPolicy '%v'는 Default Deny가 아닙니다. podSelector를 {}로 설정하세요.", [input.metadata.name])
}

# 포트 22 (SSH) Ingress 허용 차단
deny[msg] {
  input.kind == "NetworkPolicy"
  rule := input.spec.ingress[_]
  port := rule.ports[_]
  port.port == 22
  msg := sprintf("NetworkPolicy '%v'가 포트 22 Ingress를 허용합니다. SSH는 Kubernetes 내부에서 직접 허용하지 마세요.", [input.metadata.name])
}

PR 단계에서 이 검사를 통과하지 못하면 병합이 차단됩니다. 이 가드레일을 도입한 후, 네트워크 정책 관련 인시던트가 눈에 띄게 줄었습니다. Kubernetes 공식 문서에서도 NetworkPolicy 테스트를 위해 network-policy-api 프로젝트를 활용하는 방법을 언급하고 있으니 참고할 만합니다.

자주 묻는 질문 (FAQ)

Q. Cilium과 Istio를 함께 쓰면 설정이 충돌하지 않나요?

충돌하지 않습니다. Cilium과 Istio는 계층이 달라 공존할 수 있으며, 오히려 보완 관계입니다. 다만 Cilium의 L7 정책과 Istio의 AuthorizationPolicy가 동일 트래픽을 이중으로 평가할 수 있으므로, 역할을 명확히 분리해 중복 정의를 최소화하는 것이 운영 편의상 좋습니다. Cilium 공식 문서에도 Istio와의 통합 가이드가 별도로 제공됩니다.

Q. Egress Gateway를 쓰면 레이턴시가 늘어나지 않나요?

Egress 노드를 거치는 추가 홉이 생기므로 이론적으로 레이턴시가 소폭 증가합니다. Cilium eBPF 기반 구현에서는 실측 오버헤드가 1~2ms 수준으로 대부분의 워크로드에서 무시할 만합니다. 레이턴시에 민감한 서비스라면 Egress 노드를 각 가용 영역(AZ)에 분산 배치해 네트워크 경로를 최단화하는 방식으로 영향을 최소화할 수 있습니다.

Q. Zero Trust 정책 적용 후 기존 서비스가 통신 불가 상태가 되면 어떻게 복구하나요?

Default Deny를 적용하기 전에 반드시 감사(Audit) 모드로 먼저 실행하는 것을 권장합니다. Cilium의 경우 policyAuditMode: true 설정으로 정책 위반을 차단하지 않고 로그만 남길 수 있습니다. Istio는 PeerAuthenticationPERMISSIVE 모드로 설정해 mTLS를 강제하지 않고 먼저 관찰합니다. 두 도구 모두 감사 로그를 보고 누락된 허용 규칙을 파악한 후에 STRICT/차단 모드로 전환하는 점진적 접근이 안전합니다.

Q. 클러스터 간 mTLS에서 인증서는 어떻게 관리하나요?

Istio 멀티클러스터 환경에서는 cert-manager와 공유 루트 CA(Certificate Authority)를 사용하는 것이 일반적입니다. 각 클러스터에 동일한 루트 CA에서 발급된 중간 CA를 배포하면, 클러스터가 달라도 SPIFFE/SVID 기반 상호 인증이 가능합니다. HashiCorp Vault를 PKI 백엔드로 활용하면 인증서 수명 주기를 중앙에서 관리할 수 있어 대규모 멀티클러스터 환경에서 특히 유용합니다.

Q. NetworkPolicy를 잘못 적용해 DNS가 끊겼을 때 빠른 복구 방법은 무엇인가요?

kubectl delete networkpolicy --all -n <namespace> 로 해당 네임스페이스의 모든 정책을 즉시 삭제하는 것이 가장 빠른 응급 복구 방법입니다. Default Deny를 클러스터 전체(CiliumClusterwideNetworkPolicy)로 적용한 경우에는 해당 리소스를 삭제해야 합니다. 이후 DNS 허용 규칙을 포함한 정확한 정책을 다시 적용하는 순서로 복구합니다. 이 때문에 정책 변경은 항상 스테이징 클러스터에서 먼저 검증하는 습관이 중요합니다.

트래픽 격리, 설계부터 자동화까지 한 번에 완성하기

Kubernetes 멀티클러스터 네트워크 정책 설정은 "YAML 몇 장 쓰기"가 아니라 Egress Gateway로 출구를 통제하고, 네임스페이스 단위 Zero Trust로 내부를 잠그고, CI/CD로 정책 변경을 검증하는 세 축을 함께 세우는 작업입니다.

오늘 당장 시작할 수 있는 첫 번째 액션은 스테이징 클러스터에서 CiliumClusterwideNetworkPolicy의 Audit 모드를 켜는 것입니다. 정책을 차단 없이 로그만 남기는 상태로 일주일 운영하면, 현재 클러스터에서 어떤 트래픽이 기준 위반 상태인지 파악할 수 있습니다. 그 데이터를 바탕으로 허용 정책을 점진적으로 추가하고, 충분히 검증된 후 STRICT 모드로 전환하면 안전하게 Zero Trust 체계를 완성할 수 있습니다.

CNI 비교, ClusterMesh YAML 설정, 트러블슈팅 패턴은 Kubernetes 멀티클러스터 네트워크 정책 완전 가이드를 함께 참고하면 전체 그림을 완성하는 데 도움이 됩니다.