콘텐츠로 이동

12. NetworkPolicy와 CNI

Pod 사이의 통신을 막는 법

Kubernetes 네트워크 모델의 전제 —

flowchart LR
    subgraph NS1["네임스페이스 prod"]
      A["Pod A"]
      B["Pod B"]
    end
    subgraph NS2["네임스페이스 dev"]
      C["Pod C"]
    end
    A <-->|"NAT 없이 직접 ✅"| B
    A <-->|"네임스페이스가 달라도 ✅"| C
    B <-->|"노드가 달라도 ✅"| C

    classDef pod fill:#dcfce7,stroke:#16a34a,color:#14532d
    class A,B,C pod
  • 모든 Pod은 모든 Pod과 NAT 없이 통신할 수 있다
  • 네임스페이스가 달라도 마찬가지다. 네임스페이스는 네트워크 경계가 아니다
  • 노드가 달라도 마찬가지다

아무것도 안 하면 클러스터 안은 완전히 평평하다.
NetworkPolicy는 이 기본값을 선택적으로 좁히는 도구다.

CNI — 누가 네트워크를 만드는가

섹션 제목: “CNI — 누가 네트워크를 만드는가”
sequenceDiagram
    participant KL as kubelet
    participant CNI as CNI 플러그인
    participant POD as Pod 네트워크 네임스페이스
    KL->>CNI: Pod 생성 — ADD 호출
    Note over CNI: 설정 /etc/cni/net.d/<br/>바이너리 /opt/cni/bin/
    CNI->>POD: IP 할당 · veth 인터페이스 연결 · 라우팅
    CNI-->>KL: 결과 반환
    Note over KL: 실패하면 Pod 은 ContainerCreating 에서 멈춘다
  • kubelet은 Pod을 만들 때 CNI 플러그인을 호출해 IP를 할당하고 인터페이스를 붙인다
  • 설정은 /etc/cni/net.d/ 에 있고, 바이너리는 /opt/cni/bin/ 에 있다
  • CNI가 없거나 고장나면 Pod은 ContainerCreating 에서 멈춘다
CNI 특징 NetworkPolicy
Calico 널리 쓰인다. BGP 또는 오버레이 지원 (확장 정책도)
Cilium eBPF 기반. L7 정책까지 지원 (강력)
Flannel 가장 단순한 오버레이 미지원
Weave 오버레이 지원
Terminal window
kubectl get pods -n kube-system | grep -Ei 'calico|cilium|flannel|weave'
ls /etc/cni/net.d/
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-policy
namespace: prod # ← 정책은 네임스페이스 안에서만 적용된다
spec:
podSelector: # 누구에게 적용할 것인가
matchLabels: { app: api }
policyTypes: # 어느 방향을 통제할 것인가
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels: { app: web }
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels: { app: db }
ports:
- protocol: TCP
port: 5432

네 부분이 각각 다른 질문에 답한다.

flowchart LR
    P["NetworkPolicy"]
    P --> Q1["podSelector<br/>누구에게 적용하나"]
    P --> Q2["policyTypes<br/>어느 방향을 통제하나"]
    P --> Q3["ingress.from<br/>들어오는 것 중 무엇을 허용하나"]
    P --> Q4["egress.to<br/>나가는 것 중 무엇을 허용하나"]

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class Q1,Q2 key
    class Q3,Q4 mute

1 · 걸리는 순간 기본 거부

정책이 하나라도 붙으면 그 Pod은 기본 거부가 된다. 명시적으로 허용한 것 외에는 전부 막힌다.

2 · 정책은 더해진다

여러 정책이 같은 Pod에 걸리면 허용의 합집합이다. 순서도, 우선순위도, deny 규칙도 없다.

3 · 없는 방향은 통제되지 않는다

policyTypes: [Ingress]만 쓰면 egress는 전혀 제한되지 않는다.

첫 번째 규칙이 가장 중요하다 — 정책 하나가 스위치를 뒤집는다.

flowchart LR
    S1["정책이 하나도 없다"] --> A1["전부 허용 ✅"]
    S2["정책이 하나라도 걸렸다"] --> A2["기본 거부로 전환"]
    A2 --> A3["명시적으로 허용한 것만 통과"]
    A2 --> A4["나머지는 전부 차단 ❌"]

    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    class A1,A3 ok
    class A4 bad
    class A2 warn

이 셋을 알면 대부분의 혼란이 정리된다. 특히 “막는 정책”을 쓰려 하지 말 것 — 막는 방법은 허용하지 않는 것뿐이다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: prod
spec:
podSelector: {} # 빈 셀렉터 = 이 네임스페이스의 모든 Pod
policyTypes:
- Ingress
- Egress
# ingress / egress 규칙이 없다 = 아무것도 허용하지 않는다
podSelector
{} (빈 값) 네임스페이스의 모든 Pod
matchLabels: {app: api} 라벨이 맞는 Pod만

표준 패턴 — 화이트리스트

  1. default-deny-all을 먼저 깐다

    네임스페이스 전체가 기본 거부로 바뀐다.

  2. 필요한 통신만 정책을 추가해 뚫는다

    정책은 더해지므로, 허용 규칙을 하나씩 얹으면 된다.

  3. egress를 켰다면 DNS를 잊지 않는다

    UDP/TCP 53을 안 열면 이름 해석부터 죽는다. 아래 절에서 다룬다.

ingress:
- from:
- podSelector: # ① 같은 네임스페이스의 Pod
matchLabels:
app: web
- namespaceSelector: # ② 다른 네임스페이스의 모든 Pod
matchLabels:
kubernetes.io/metadata.name: frontend
- ipBlock: # ③ IP 대역 (클러스터 밖 포함)
cidr: 10.0.0.0/16
except:
- 10.0.5.0/24
flowchart LR
    T["대상 Pod<br/>podSelector 로 지정"]
    S1["① podSelector<br/>같은 네임스페이스 안에서만 찾는다"] --> T
    S2["② namespaceSelector<br/>다른 네임스페이스 전체"] --> T
    S3["③ ipBlock<br/>패킷의 소스 IP · 클러스터 밖 포함"] --> T

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class T key
    class S1,S2,S3 mute
  • podSelector 만 쓰면 정책이 있는 네임스페이스 안에서만 찾는다
  • 모든 네임스페이스에는 kubernetes.io/metadata.name 라벨이 자동으로 붙어 있다 — 이걸로 지정하면 편하다
  • ipBlockPod IP가 아니라 패킷의 소스 IP 기준이다
from:
- namespaceSelector:
matchLabels:
team: frontend
- podSelector:
matchLabels:
app: web

“frontend 네임스페이스의 모든 Pod” 또는 “같은 네임스페이스의 app=web Pod”

flowchart LR
    A["frontend 네임스페이스의<br/>모든 Pod"] --> T["허용"]
    B["같은 네임스페이스의<br/>app=web Pod"] --> T

    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    class A,B,T ok
spec:
podSelector:
matchLabels: { app: api }
policyTypes: [Egress]
egress:
- to:
- podSelector:
matchLabels: { app: db }
ports:
- protocol: TCP
port: 5432
- to: # ★ DNS를 반드시 열어야 한다
- namespaceSelector: # ← 이 둘은 같은 항목 = AND
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }

DNS를 안 열면 증상이 엉뚱한 곳에서 나타난다.

flowchart LR
    P["egress 정책을 켰다"] --> D["CoreDNS 로 가는<br/>UDP 53 도 막힌다"]
    D --> N["이름 해석 실패"]
    N --> S["증상: 연결 거부가 아니라<br/>Name or service not known"]
    S --> W["엉뚱하게 Service · 앱을 의심하게 된다 ❌"]

    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class W bad
    class D,N,S warn
    class P mute
ports:
- protocol: TCP
port: 8080
- protocol: TCP
port: http # Pod의 이름 있는 포트도 가능
- protocol: TCP
port: 8000
endPort: 9000 # 범위
  • ports생략하면 모든 포트가 허용된다
  • port는 Pod의 포트(targetPort) 다. Service의 port가 아니다

NetworkPolicy는 Service를 모른다.

flowchart LR
    C["클라이언트"] --> SVC["Service<br/>port 80 → targetPort 8080"]
    SVC --> POD["Pod<br/>8080 에서 수신"]
    NP["NetworkPolicy"] -.->|"여기를 본다<br/>→ 8080 을 써야 한다"| POD
    NP -.->|"이건 안 본다 ❌"| SVC

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class POD key
    class SVC bad
    class C,NP mute

같은 이유로 NetworkPolicy로 Service를 막을 수는 없다. 막히는 것은 최종적으로 도달하는 Pod이다.

# db는 api에서 오는 5432만 받는다
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-api
namespace: prod
spec:
podSelector:
matchLabels:
tier: db
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels:
tier: api
ports:
- protocol: TCP
port: 5432

정책 하나가 어디까지 바꾸는지 보면 감이 온다.

flowchart LR
    WEB["web<br/>정책 없음"] -->|"아무 데나 갈 수 있다 ✅"| API["api<br/>정책 없음"]
    API -->|"5432 허용 ✅"| DB["db<br/>tier=db · 정책 걸림"]
    WEB -.->|"차단 ❌"| DB
    EXT["다른 네임스페이스"] -.->|"차단 ❌"| DB
    API -->|"egress 는 여전히 자유 ⚠️"| OUT["외부 · 아무 데나"]

    classDef locked fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef free fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    class DB locked
    class WEB,API free
    class OUT warn
  • 이 정책 하나로 db Pod의 다른 모든 인바운드가 막힌다
  • api Pod에는 아무 정책도 안 걸렸으므로 api는 여전히 아무 데나 갈 수 있다
  • 진짜 잠그려면 각 계층마다 정책이 필요하다
Terminal window
kubectl get netpol -A
kubectl describe netpol db-allow-api -n prod # 해석된 규칙을 보여준다
# 실제로 되는지 확인 — 이게 가장 확실하다
kubectl run tmp --image=busybox:1.36 --rm -it --restart=Never -n prod -- sh
wget -qO- --timeout=3 http://api-svc:8080
nc -zv 10.244.2.7 5432
# 라벨이 정말 맞는지
kubectl get pods -n prod --show-labels
kubectl get ns --show-labels
flowchart TD
    S{"증상"}
    S -->|"정책을 만들었는데 안 막힌다"| A["CNI 가 NetworkPolicy 를 지원하지 않는다<br/>Flannel 기본 설정을 의심"]
    S -->|"의도보다 많이 막힌다"| B{"무엇을 확인하나"}
    B --> B1["AND/OR 하이픈 위치"]
    B --> B2["egress 라면 DNS 허용 여부"]
    B --> B3["포트를 Service 포트로 쓰지 않았나"]
    S -->|"특정 네임스페이스만 안 된다"| C["namespaceSelector 라벨 불일치<br/>kubectl get ns --show-labels"]
    S -->|"이름 해석부터 실패"| D["egress 에서 UDP 53 미허용"]

    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class A,D bad
    class B1,B2,B3,C warn
    class S,B mute
증상 원인 후보
정책을 만들었는데 안 막힌다 CNI가 NetworkPolicy를 지원하지 않는다
의도보다 많이 막힌다 AND/OR 실수, DNS 미허용, 포트를 Service 포트로 씀
특정 네임스페이스만 안 된다 namespaceSelector 라벨 불일치
이름 해석부터 실패 egress에서 UDP 53 미허용
  • 기본은 전부 허용. NetworkPolicy는 좁히는 도구다
  • 정책을 강제하는 것은 CNI다. 미지원 CNI에서는 조용히 무시된다
  • 정책이 하나라도 걸리면 그 Pod은 기본 거부로 바뀐다
  • 정책은 더해질 뿐이다. deny 규칙도, 우선순위도 없다
  • policyTypes에 없는 방향은 통제되지 않는다
  • podSelector: {} = 네임스페이스 전체 → default-deny-all 패턴
  • 하이픈 위치가 AND와 OR를 가른다 — 가장 흔한 실수
  • egress를 켜면 DNS(UDP/TCP 53)를 반드시 열어야 한다
  • 포트는 Pod의 포트다. Service 포트가 아니다