본문 바로가기
Cloud Computing & MSA

NetworkPolicy로 파드 통신 기본 차단하기

아주라·2026년 9월 18일·조회 0

클러스터를 새로 받아서 파드를 몇 개 띄워보면 대개 놀란다. 아무 정책도 없는 상태에서는 네임스페이스가 달라도 파드끼리 서로 다 붙는다. 프론트엔드 파드에 셸을 열고 DB 파드 포트로 붙어보면 그냥 연결된다. 예전에 온프레미스 클러스터를 넘겨받아 점검하다가, 결제 서비스 파드에서 아무 관계 없는 배치 파드로 telnet이 붙는 걸 보고 정책부터 손댔던 기억이 있다.

결론부터 말하자면, 제로트러스트의 시작은 기본 deny-all NetworkPolicy다. 네임스페이스마다 ingress와 egress를 모두 막는 정책을 하나 깔고, 그 위에 꼭 필요한 통신만 라벨과 네임스페이스 기준으로 열어준다. egress의 외부 도메인 제어까지 가려면 Cilium의 toFQDNs 같은 CNI 확장이 필요하다.

1. NetworkPolicy가 무엇이고 왜 필요한가

NetworkPolicy는 파드 단위로 어떤 트래픽을 받고 내보낼지 정하는 쿠버네티스 리소스다. 방화벽 규칙을 파드 라벨에 붙인다고 보면 된다. 기본값은 열림이다. 파드는 정책이 하나도 자기를 선택하지 않는 한 비격리(non-isolated) 상태이고, 인바운드와 아웃바운드 모두 제한이 없다.

주의할 점이 하나 있다. NetworkPolicy는 이를 지원하는 CNI 플러그인이 있어야 실제로 동작한다. Calico, Cilium, Weave Net 같은 플러그인이 정책을 강제한다. 지원하지 않는 CNI에서는 정책을 만들어도 API 서버에 저장만 될 뿐 트래픽에 아무 영향이 없다. 먼저 자기 클러스터 CNI가 NetworkPolicy를 강제하는지 확인해야 한다.

동작 방식에서 기억할 것 두 가지다. 첫째, 정책은 가산적(additive)이다. 여러 정책이 같은 파드를 선택하면 각 정책의 허용 규칙이 합쳐진다. 규칙끼리 충돌해 서로 막는 개념이 없다. 둘째, 통신이 성립하려면 출발지의 egress 규칙과 도착지의 ingress 규칙이 모두 허용해야 한다. 한쪽만 열려 있으면 연결되지 않는다.

2. 기본 deny-all 정책 깔기

가장 먼저 네임스페이스 안 모든 파드의 ingress와 egress를 막는다. 핵심은 podSelector: {}다. 빈 셀렉터는 해당 네임스페이스의 모든 파드를 선택한다는 뜻이다. policyTypesIngressEgress를 넣되 실제 허용 규칙(ingress, egress 필드)은 비워두면, 선택된 파드는 격리 상태가 되어 아무것도 주고받지 못한다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: app
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

적용하고 확인한다.

$ kubectl apply -f default-deny-all.yaml
networkpolicy.networking.k8s.io/default-deny-all created

$ kubectl -n app get networkpolicy
NAME               POD-SELECTOR   AGE
default-deny-all   <none>         5s

여기서 거의 모두가 한 번 걸린다. egress까지 막는 순간 DNS 조회가 죽는다. 파드가 backend 같은 서비스 이름을 IP로 풀려면 kube-system의 kube-dns(CoreDNS)로 53번 포트 질의를 보내야 하는데, deny-all egress가 그 질의부터 막아버린다. 그래서 이름 해석이 안 되고 애플리케이션은 "Name or service not known" 류 에러로 죽는다.

그래서 deny-all egress를 쓸 때는 DNS 허용 정책을 짝으로 함께 넣는다. 네임스페이스에 자동으로 붙는 kubernetes.io/metadata.name 라벨로 kube-system을 지정하고 53번 포트만 연다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: app
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

3. 라벨과 네임스페이스로 필요한 통신만 열기

이제 기본은 다 막혔으니 그 위에 구멍을 낸다. 예를 들어 app: backend 파드가 app: frontend 파드로부터 8080 포트 요청만 받게 하려면, backend를 대상으로 하는 ingress 정책을 만든다. from 아래에 podSelector를 두면 같은 네임스페이스 안에서 라벨이 맞는 파드만 허용한다.

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

다른 네임스페이스에서 오는 트래픽을 허용하려면 namespaceSelector를 쓴다. 여기서 흔한 함정이 하나 있다. from 목록에서 namespaceSelectorpodSelector하나의 항목 안에 같이 쓰면 "그 네임스페이스 안에서 그 라벨을 가진 파드"라는 AND 조건이 된다. 반대로 목록의 별개 항목(각각 앞에 -)으로 두면 OR 조건이 된다. 이 둘을 헷갈리면 열려고 한 것보다 넓게 또는 좁게 열린다.

  # AND: monitoring 네임스페이스의 app=prometheus 파드만 허용
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
      podSelector:
        matchLabels:
          app: prometheus
    ports:
    - protocol: TCP
      port: 9100

정책을 하나 만든 순간 해당 파드는 그 방향으로 격리된다. 즉 backend에 ingress 정책을 붙이면 위에서 허용한 frontend 외의 모든 인바운드는 자동으로 막힌다. 명시적 deny 규칙을 따로 쓸 필요가 없다. 표준 NetworkPolicy에는 deny 규칙 자체가 없고, 허용하지 않은 것은 격리로 인해 막히는 구조다.

4. Cilium으로 egress를 도메인 단위까지 막기

표준 NetworkPolicy의 egress는 목적지를 podSelector, namespaceSelector, ipBlock(CIDR)으로만 지정할 수 있다. 문제는 외부 API다. api.github.com 같은 도메인은 IP가 자주 바뀌어서 CIDR로 고정하기 어렵다. 여기서 Cilium이나 Calico 같은 CNI의 확장 정책이 필요하다.

Cilium은 cilium.io/v2 API의 CiliumNetworkPolicy로 도메인 기반 egress를 지원한다. toFQDNs에 허용할 도메인을 적으면 Cilium이 파드의 DNS 응답을 관찰해 해당 도메인의 IP를 학습하고, 그 IP로 나가는 트래픽만 연다. 이게 동작하려면 DNS 질의를 Cilium이 볼 수 있어야 하므로, kube-dns로 가는 53번 egress에 rules.dns를 붙여 DNS를 가로채도록 함께 열어준다.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: allow-fqdn-egress
  namespace: app
spec:
  endpointSelector:
    matchLabels:
      app: backend
  egress:
  - toEndpoints:
    - matchLabels:
        "k8s:io.kubernetes.pod.namespace": kube-system
        "k8s:k8s-app": kube-dns
    toPorts:
    - ports:
      - port: "53"
        protocol: ANY
      rules:
        dns:
        - matchPattern: "*"
  - toFQDNs:
    - matchName: "api.github.com"
    toPorts:
    - ports:
      - port: "443"
        protocol: TCP

여기서 matchName은 정확히 일치하는 도메인을, matchPattern은 와일드카드 패턴을 뜻한다. DNS 규칙의 matchPattern: "*"는 모든 조회를 관찰하되 실제 나가는 연결은 toFQDNs에서 허용한 도메인으로만 제한한다. Calico를 쓴다면 표준 NetworkPolicy의 egress로 CIDR 기반 제어를 하거나, projectcalico.org/v3의 GlobalNetworkPolicy로 클러스터 전역 규칙을 건다. 다만 도메인(FQDN) 기반 egress가 필요하면 Cilium의 toFQDNs가 오픈소스에서 바로 쓰기 편하다.

5. 정책이 실제로 막고 있는지 검증

정책은 만들었다고 끝이 아니라 실제로 막히고 열리는지 확인해야 한다. 임시 클라이언트 파드를 하나 띄워 붙여본다.

$ kubectl -n app run tester --image=busybox --restart=Never -it --rm -- sh

# 허용되지 않은 파드로 접근 - 막혀야 정상
/ # wget -qO- --timeout=3 http://backend:8080
wget: download timed out

# 허용한 경로에서 접근 - 열려야 정상
/ # nslookup backend
Server:    10.96.0.10
Address:   10.96.0.10:53
Name:      backend.app.svc.cluster.local
Address:   10.108.42.10

차단은 대개 거부(RST)가 아니라 타임아웃으로 나타난다. NetworkPolicy는 패킷을 조용히 버리기 때문에 wget: download timed out이나 연결 대기 상태가 보이면 정책이 막고 있다는 신호다. 반대로 DNS가 안 풀리면 3절의 allow-dns-egress 정책을 빠뜨린 것이다.

Cilium 클러스터라면 드롭 사유를 직접 볼 수 있다. 특정 파드에서 무엇이 왜 막히는지 확인할 때 유용하다.

$ kubectl -n kube-system exec ds/cilium -- cilium monitor --type drop
xx drop (Policy denied) flow 0x0 to endpoint 1234, ... 10.0.1.5 -> 10.0.2.7 tcp SYN

# 엔드포인트별 정책 적용 상태
$ kubectl -n kube-system exec ds/cilium -- cilium endpoint list

정책 적용 대상과 규칙을 다시 보고 싶으면 describe가 가장 빠르다.

$ kubectl -n app describe networkpolicy allow-frontend-to-backend
Name:         allow-frontend-to-backend
Namespace:    app
Spec:
  PodSelector:     app=backend
  Allowing ingress traffic:
    To Port: 8080/TCP
    From:
      PodSelector: app=frontend
  Policy Types: Ingress

6. 정리

제로트러스트 파드 네트워킹의 뼈대는 네임스페이스마다 default-deny-all을 깔고, DNS egress 허용을 짝으로 넣은 뒤, 라벨과 네임스페이스 기준으로 필요한 통신만 여는 순서다. 표준 NetworkPolicy는 파드와 CIDR 단위까지 다루고, 외부 도메인 단위 egress 제어나 드롭 관측이 필요하면 Cilium의 CiliumNetworkPolicy와 toFQDNs를 얹는다. 정책을 넣은 뒤에는 반드시 임시 파드로 막힘과 열림을 양쪽 다 확인하는 습관을 들이는 편이 사고를 줄인다.

자주 묻는 질문

NetworkPolicy를 만들었는데 트래픽이 그대로 통과한다. 왜 그런가?

사용 중인 CNI 플러그인이 NetworkPolicy 강제를 지원하지 않는 경우다. NetworkPolicy는 Calico, Cilium, Weave Net처럼 정책을 강제하는 CNI가 있어야 동작한다. 지원하지 않는 CNI에서는 리소스가 API 서버에 저장만 되고 트래픽에는 아무 영향이 없다. 클러스터 CNI가 NetworkPolicy를 지원하는지 먼저 확인해야 한다.

deny-all egress를 적용했더니 파드가 이름 해석에 실패한다.

egress를 전부 막으면 kube-dns(CoreDNS)로 가는 53번 포트 질의까지 막혀 DNS 조회가 실패한다. kube-system 네임스페이스로 UDP와 TCP 53번을 허용하는 egress 정책을 deny-all과 짝으로 함께 적용해야 한다. namespaceSelector에는 자동으로 붙는 kubernetes.io/metadata.name 라벨을 쓰면 편하다.

namespaceSelector와 podSelector를 함께 쓰면 어떻게 해석되나?

하나의 from 또는 to 항목 안에 두 셀렉터를 같이 두면 AND 조건이 되어 '그 네임스페이스 안에서 라벨이 맞는 파드'만 허용한다. 목록의 별개 항목으로(각각 하이픈으로) 나누면 OR 조건이 되어 두 대상을 각각 허용한다. 의도한 범위와 다르게 열릴 수 있으니 들여쓰기를 반드시 확인한다.

특정 외부 도메인으로 나가는 egress만 허용하려면?

표준 NetworkPolicy는 egress 목적지를 CIDR(ipBlock)까지만 지정할 수 있어 IP가 자주 바뀌는 도메인에는 부적합하다. Cilium의 CiliumNetworkPolicy에서 toFQDNs의 matchName 또는 matchPattern으로 도메인 단위 허용이 가능하다. 이때 kube-dns로 가는 53번 egress에 rules.dns를 붙여 Cilium이 DNS 응답을 관찰하도록 함께 열어야 한다.

정책이 트래픽을 막을 때 어떤 증상으로 나타나나?

NetworkPolicy는 패킷을 거부(RST)하지 않고 조용히 버린다. 그래서 연결 거부가 아니라 타임아웃이나 대기 상태로 나타난다. wget에서 download timed out이 뜨거나 연결이 계속 걸려 있으면 정책이 막고 있다는 신호다. Cilium이라면 cilium monitor --type drop으로 Policy denied 드롭을 직접 확인할 수 있다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.