1. 개요
NetworkPolicy 는 문서가 짧고 예제가 많아 쉬워 보인다. 실제로 세 네임스페이스에 default-deny 를 세우면서 다섯 번 놀랐다. 전부 “정책이 있는데 아무것도 안 막는다” 또는 “정책이 막아야 할 것 말고 다른 걸 막는다” 였다. 그때마다 의미론을 다시 읽었다. 그 다섯 가지를 정리한다.
2. 핵심 내용
2-1. 정책이 파드를 선택하는 순간 격리가 시작된다
NetworkPolicy 는 allow 규칙만 쓴다. deny 규칙이 없다. 그러면 “차단” 은 어디서 오나. 어떤 정책이든 파드를 선택하면, 그 파드는 그 방향(ingress/egress)에 대해 격리되고, 선택한 정책들의 allow 합집합만 통과한다.
그래서 default-deny 는 이렇게 생겼다.
spec:
podSelector: {} # 네임스페이스의 모든 파드 선택
policyTypes: [Ingress] # ingress 방향 격리 시작
# ingress 규칙 없음 → 아무것도 허용 안 함규칙이 비어 있는 게 핵심이다. podSelector: {} 가 모든 파드를 선택해 격리를 켜고, 허용 규칙이 없으니 전부 막힌다. 그 위에 별도 정책으로 허용을 얹는다.
반대로 말하면, 허용 정책만 있고 default-deny 가 없으면 허용 정책이 선택한 파드만 격리되고 선택되지 않은 파드는 전부 열려 있다. 새 Deployment 를 추가하고 정책을 잊으면 그 파드는 열린 채다. default-deny 가 안전망인 이유다.
2-2. ipBlock 에 파드 CIDR 을 넣으면 무력화된다
초안에 이런 게 있었다.
ingress:
- from:
- ipBlock:
cidr: 10.42.0.0/16 # "클러스터 안은 허용" 의도k3s 기본 파드 CIDR 이다. 의도는 “클러스터 내부만” 이었는데, 모든 파드가 이 대역에 있으니 모든 파드를 허용 하는 규칙이다. default-deny 위에 이걸 얹으면 default-deny 가 없는 것과 같다. 차단이 0 이었다.
클러스터 안의 출처는 podSelector/namespaceSelector 로 골라야 한다. ipBlock 은 클러스터 밖 주소용이다. 대조군으로 확인하는 방법: 정책이 실제로 뭔가 막고 있으면 어딘가 RESTARTS 나 connection refused 가 생긴다. 아무 변화가 없으면 아무것도 안 막고 있는 것이다.
2-3. kubelet 프로브도 막힌다
default-deny ingress 를 넣으면 liveness/readiness 프로브가 실패해 파드가 재시작 루프를 돈다. 프로브는 노드의 kubelet 이 파드 IP 로 보낸다. 출처가 파드가 아니라 노드다.
노드 주소를 ipBlock 으로 허용해야 한다. 노드가 셋이면 /32 셋. 노드가 늘면 정책을 고쳐야 한다. 이건 실제로 잊기 쉬워서, 노드 추가 절차 문서에 “NetworkPolicy 노드 목록 갱신” 을 넣어 뒀다.
다른 네임스페이스(예: argocd)의 파드가 40일 동안 RESTARTS=1 인 것을 보고 “저 네임스페이스에는 프로브 문제가 없다 → 정책이 없다” 를 확인하는 식으로 대조군으로 썼다.
2-4. IPSet 반영 지연이 Job 을 죽인다
정책이 허용해도 새 파드가 뜨고 그 IP 가 IPSet 에 들어가기까지 몇 초 가 있다. 그 사이 연결은 ECONNREFUSED 다. 오래 사는 Deployment 는 재시도가 알아서 넘기지만, 뜨자마자 DB 에 붙는 CronJob 은 이 창에서 실패하고 Job 이 끝난다. 백업 CronJob 이 이렇게 며칠 조용히 실패했다.
# 백업 스크립트 앞
for i in $(seq 1 20); do
pg_isready -h "$DB_HOST" -t 3 && break
sleep 3
doneJob 성격의 워크로드는 첫 연결 전에 대기 루프를 둔다. 그리고 실패를 텔레그램 같은 곳으로 알려야 “며칠 조용히” 가 안 생긴다.
2-5. 거부는 파드 안에서만 보인다
LAN 전용 정책(집 안 네트워크에서만 접근)을 만들고 맥에서 curl 로 확인했다. 허용돼야 할 곳도 되고, 거부돼야 할 곳도 됐다. 정책이 안 먹는 줄 알았다.
맥의 요청은 홈랩 방화벽에서 SNAT 돼 방화벽 내부 주소로 클러스터에 도착한다. 그 주소가 허용 대역 안이다. 맥에서는 거부를 볼 수 없다.
kubectl run probe --rm -it --image=curlimages/curl -n other-ns -- \
curl -m 3 http://target.ns.svc.cluster.local:8080/거부는 파드 안에서만 보인다. 임시 파드를 다른 네임스페이스에 띄워 확인한다. 정책 검증 절차에 이 한 줄이 없으면 “맥에서 되니까 됐다” 로 끝난다.
2-6. egress 는 부분 적용이 없다
ingress default-deny 는 “밖에서 못 들어온다” 라 비교적 안전하다. egress default-deny 는 앱이 나가는 모든 곳을 알아야 한다. DNS 부터.
# DNS 는 UDP 와 TCP 둘 다
egress:
- to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}}]
ports:
- {protocol: UDP, port: 53}
- {protocol: TCP, port: 53}TCP 53 을 빠뜨리면 큰 응답(DNSSEC, 많은 A 레코드)이 UDP 에서 잘려 TCP 로 재시도할 때 막힌다. 가끔만 실패해서 찾기 어렵다.
외부 API(OAuth, 푸시, Turnstile, SMTP) 는 IP 를 고정하지 않으니 “인터넷 전체 허용 - 사설 대역 except” 모양이 된다.
- to:
- ipBlock:
cidr: 0.0.0.0/0
except: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16]그리고 한 커밋 으로 넣는다. default-deny 를 먼저 넣고 허용을 다음 커밋에 넣으면 그 사이 앱이 죽는다. 이미 egress 정책이 없는 네임스페이스에서 siteverify·SMTP·텔레그램·iCal 같은 아웃바운드가 있으면, 전수조사가 끝나기 전엔 egress 를 건드리지 않는 것도 선택이다. 그 판단도 주석으로 남긴다.
3. 마무리
요약
- 격리는 “정책이 파드를 선택하는 순간” 시작된다. default-deny 는 빈 규칙으로 격리만 켠다.
ipBlock에 파드 CIDR 은 전체 허용이다. 클러스터 안은 selector 로.- 프로브는 노드에서 온다. 노드
/32를 허용하고 노드 추가 절차에 넣는다.- IPSet 반영 지연 몇 초. Job 은 대기 루프.
- 거부는 파드 안에서만 보인다. 맥은 SNAT 에 속는다.
- egress 는 전수조사 후 한 커밋. DNS 는 UDP + TCP.