콘텐츠로 이동
Study NoteCKA

NetworkPolicy와 CNI

Pod 사이의 통신을 막는 법

Kubernetes 네트워크 모델의 전제 —

Pod 은 네임스페이스가 달라도 노드가 달라도 NAT 없이 서로 직접 통신할 수 있다는 기본 전제
  • 모든 Pod은 모든 Pod과 NAT(Network Address Translation — 주소 변환) 없이 통신할 수 있다
  • 네임스페이스가 달라도 마찬가지다. 네임스페이스는 네트워크 경계가 아니다
  • 노드가 달라도 마찬가지다

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

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

섹션 제목: “CNI — 누가 네트워크를 만드는가”

쿠버네티스는 위의 네트워크 모델을 요구사항으로 정의만 하고 직접 구현하지 않는다. 구현이 없으면 Pod은 IP조차 받지 못한다. 그 구현을 맡아 “평평한 네트워크”를 실제로 만드는 주체가 CNI(Container Network Interface — 컨테이너 런타임이 Pod 네트워크를 만들 때 호출하는 표준 인터페이스)다.

kubelet 의 샌드박스 생성 요청을 받은 컨테이너 런타임이 CNI 플러그인의 ADD 를 호출해 IP 할당 · veth 연결 · 라우팅을 맡기고 실패하면 ContainerCreating 에서 멈추는 순서
  • kubelet이 Pod 샌드박스를 요청하면 컨테이너 런타임이 CNI 플러그인을 호출해 IP를 할당하고 veth(virtual ethernet — Pod과 노드를 잇는 한 쌍의 가상 케이블) 인터페이스를 붙인다. v1.24부터 CNI 관리는 kubelet이 아니라 런타임의 몫이다
  • 설정은 /etc/cni/net.d/ 에 있고, 바이너리는 /opt/cni/bin/ 에 있다 — 이 경로를 읽는 쪽도 런타임이다
  • CNI가 없거나 고장나면 Pod은 ContainerCreating 에서 멈춘다
CNI특징NetworkPolicy
Calico널리 쓰인다. BGP(라우터 간 경로 교환 프로토콜) 또는 오버레이지원 (확장 정책도)
CiliumeBPF(커널 내장 프로그래밍 기술) 기반. L7 정책까지지원 (강력)
Flannel가장 단순한 오버레이미지원
Weave오버레이지원

표의 내부 구현 방식 — BGP·eBPF·오버레이(노드 사이 패킷을 캡슐화해 터널로 나르는 방식) — 은 깊게 팔 필요 없다. 시험 관점에서는 어느 CNI가 NetworkPolicy를 지원하는지와 아래 파일 경로 정도면 충분하다.

덱의 축으로 보면 NetworkPolicy도 선언(spec)일 뿐이고, 실제 상태로 만드는 쪽은 CNI다. 다만 다른 리소스와 달리 이 루프가 비어 있어도 — CNI가 정책을 지원하지 않아도 — 아무 에러가 나지 않는다. 첫머리 함정의 “조용히 실패한다”가 여기서 나온다.

터미널 창
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

NetworkPolicy는 kubectl create로 못 만든다 — 시험 중에는 공식 Network Policies 문서의 예시 YAML을 복사해 셀렉터·포트만 바꾸는 게 가장 빠르다.

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

NetworkPolicy 를 읽을 때 podSelector · policyTypes · ingress.from · egress.to 네 가지를 각각 무엇을 정하는 칸으로 볼지

1 · 걸리는 순간 기본 거부

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

2 · 정책은 더해진다

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

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

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

첫 번째 규칙이 가장 중요하다 — 정책 하나가 스위치를 뒤집는다. 따라서 더 좁은 정책을 하나 추가해 기존의 넓은 허용을 덮어쓸 수 없다. “기존 정책을 삭제하지 말라”는 문제에서는 새 후보만 비교하지 말고, 같은 Pod을 고르는 기존 정책이 이미 무엇을 허용하는지도 함께 읽는다.

정책이 하나도 없으면 전부 허용이고 하나라도 걸리면 기본 거부로 바뀌어 명시적으로 허용한 것만 통과한다는 대비

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

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
from 에 쓸 수 있는 podSelector · namespaceSelector · ipBlock 세 가지가 각각 어느 범위를 가리키는지
  • podSelector 만 쓰면 정책이 있는 네임스페이스 안에서만 찾는다
  • 모든 네임스페이스에는 kubernetes.io/metadata.name 라벨이 자동으로 붙어 있다 — 이걸로 지정하면 편하다
  • ipBlock은 Pod IP가 아니라 패킷의 소스 IP 기준이다
from:
- namespaceSelector:
matchLabels:
team: frontend
- podSelector:
matchLabels:
app: web

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

from 아래에 하이픈을 두 번 쓰면 두 조건이 OR 로 묶여 각각 따로 허용된다는 모습
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 }

셀렉터가 가리키는 것은 CoreDNS — 클러스터 내부 DNS 서버 — Pod이다. kube-system 네임스페이스에서 도는데, 라벨이 k8s-app: kube-dns 인 것은 예전 구현(kube-dns)의 라벨을 호환 목적으로 그대로 쓰기 때문이다.

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

egress 정책을 켜면 CoreDNS 로 가는 UDP 53 까지 막혀 연결 거부가 아니라 이름 해석 실패로 나타나 엉뚱한 곳을 의심하게 되는 연쇄
ports:
- protocol: TCP
port: 8080
- protocol: TCP
port: http # Pod의 이름 있는 포트도 가능
- protocol: TCP
port: 8000
endPort: 9000 # 범위

이름 있는 포트와 endPort 범위는 있다는 것만 알아두면 된다. 이 절의 포인트는 아래 둘이다.

  • ports를 생략하면 모든 포트가 허용된다
  • port는 Pod의 포트(targetPort) 다. Service의 port가 아니다

NetworkPolicy는 Service를 모른다.

NetworkPolicy 의 포트는 Pod 이 실제로 듣는 targetPort 를 보고 Service 의 port 는 보지 않는다는 대비

같은 이유로 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

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

db 에만 정책을 걸면 api 에서 5432 만 통과하고 web 과 다른 네임스페이스는 차단되지만 api 의 egress 는 여전히 자유롭다는 결과
  • 이 정책 하나로 db Pod의 다른 모든 인바운드가 막힌다
  • api Pod에는 아무 정책도 안 걸렸으므로 api는 여전히 아무 데나 갈 수 있다
  • 진짜 잠그려면 각 계층마다 정책이 필요하다
터미널 창
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
안 막힌다 · 많이 막힌다 · 특정 네임스페이스만 · 이름 해석 실패 네 증상에서 CNI 지원 · 하이픈 위치 · 라벨 · UDP 53 중 어디를 볼지 가르는 진단표
증상원인 후보
정책을 만들었는데 안 막힌다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 포트가 아니다