0. Contents
1. Overview
23장과 24장의 관계
- 23장이 “뚫린 프로세스를 컨테이너 안에 가두는” 이야기였다면, 24장은 그 컨테이너가 밖으로 뻗는 손을 자르는 이야기다
기본 상태: 평평한(flat) 네트워크
- 쿠버네티스 네트워크는 기본적으로 **평평(flat)**하다
- 노드가 달라도, 네임스페이스가 달라도, 모든 파드는 다른 모든 파드에 직접 연결할 수 있다
- 개발할 때는 아무 설정 없이 서비스끼리 붙으니 편하다
- 보안 관점에서는 파드 하나가 뚫리는 순간 그 파드가 클러스터의 나머지 전부를 두드릴 수 있다는 뜻이다
이 장의 핵심: 앱과 함께 배포하는 두 겹의 방화벽
- 이 장의 재미는 그 열린 네트워크 위에 두 겹의 방화벽을 앱과 함께 배포한다는 데 있다
- 방화벽 장비에 규칙을 넣어달라고 관리자에게 티켓을 내는 게 아니다
- 앱을 아는 개발자가 Deployment 옆에 YAML 한 장을 더 두는 것이다
- 그 YAML은 “이건 열어라"만 적는다 — 나머지는 알아서 닫힌다
2. Problem
2.1 네임스페이스는 폴더다 — 방화벽이 아니다
가장 흔한 오해
- 이 장을 읽기 전에 가장 흔한 오해부터 걷어내야 한다
- “네임스페이스로 팀을 나눴으니 격리된 것 아닌가?”
네임스페이스가 나눠주는 것과 안 나눠주는 것
| 구분 | 네임스페이스가 나눠주는 것 | 안 나눠주는 것 |
|---|---|---|
| 이름 공간 (같은 이름의 Deployment 공존) | ✅ | |
RBAC 권한 (kubectl로 남의 파드 조회) | ✅ | |
| 리소스 쿼터 | ✅ | |
| 네트워크 트래픽 | ❌ 그대로 뚫림 | |
| 노드/커널 | ❌ 공유 |
비유: 컴퓨터의 폴더
- 파일을 폴더로 정리하면 보기는 편하지만, 폴더에 넣었다고 다른 프로그램이 그 파일을 못 읽는 건 아니다
- 네임스페이스는 파드를
팀A,팀B폴더에 나눠 담을 뿐이다- 팀A 파드가 팀B 파드에게 말 거는 것을 막지 않는다
막힌 것처럼 보이는 이유 두 가지
- “근데 실제로 해보면 다른 네임스페이스 파드에 접근이 안 되던데?“라는 경험이 있을 수 있다
- 그건 네임스페이스가 막아서가 아니라 다른 두 가지 때문에 그렇게 보이는 것이다
| 하려는 것 | 되나? | 왜 |
|---|---|---|
kubectl get pods -n team-b | ❌ | RBAC이 막음 (API 서버 접근) |
curl web (짧은 이름) | ❌ | DNS가 내 네임스페이스에서만 찾음 |
curl web.team-b (NS 붙임) | ✅ | 그냥 통과 |
| 파드 IP로 직접 접속 | ✅ | 아무것도 안 막음 |
해커가 들어오는 길
- 해커는 위의 두 줄이 아니라 아래 두 줄로 들어온다
- 관리 화면(API)은 잠겨 있는데 뒷문(네트워크)은 열려 있는 상태다
- 그래서 책은 “네임스페이스는 그룹핑 개념일 뿐"이라고 못 박고 시작한다
- 바닥에 선만 그어놓은 셈이고, 그 선 위에 벽을 올리는 것이 이 장의 일이다
2.2 옆 팀이 뚫리면 나도 뚫린다 — 멀티테넌시의 그늘
멀티테넌시 환경
- 이 문제가 가장 아프게 드러나는 곳이 여러 팀이 클러스터 하나를 나눠 쓰는 멀티테넌시 환경이다
공유 클러스터의 보안 수준
- 혼자 쓰는 클러스터면 리스크가 자기 안에서 끝난다
- 공유 클러스터에서는 가장 약한 팀의 보안 수준이 클러스터 전체의 보안 수준이 된다
멀티테넌시: 직접 조립하는 부품
- 책은 멀티테넌시 자체가 쿠버네티스에 완성품으로 들어 있지 않다고 인정한다
- 부품이 흩어져 있고 직접 조립해야 한다
- 네임스페이스+RBAC(열쇠)
- 쿼터(한 집이 물·전기 다 쓰는 것 방지)
- 스토리지·네트워크 격리(남의 창고·전화)
- 공유 자원 관리(DNS·CRD) 등
- 이 장이 맡는 부품은 네트워크 격리다
- 저자 스스로 이것이 soft한 접근이라고 선을 긋는다
격리 강도 스펙트럼
- 23장 4.1의 격리 스펙트럼(컨테이너 → gVisor → Kata → VM)과 정확히 같은 구조다
- 오른쪽으로 갈수록 안전하고 비싸다
2.3 방화벽 티켓의 시대 — 앱을 아는 사람과 규칙을 짜는 사람이 다르다
옛날 방식: 방화벽 티켓
- 그럼 그냥 방화벽을 걸면 되는 것 아닌가?
- 옛날 방식은 이랬다
- 개발자: “우리 주문 서비스가 결제 서비스랑 재고 서비스에 붙어야 해요. 포트 8080이랑 5432요.”
- 관리자: “알겠습니다, 방화벽 규칙 추가할게요.” (티켓 접수, 3일 후 반영)
문제 세 가지
- 관리자는 앱을 모른다 — 앱 사정을 아는 건 코드를 짠 사람뿐
- 마이크로서비스면 선이 수백 개 — 서비스 50개가 얽힌 거미줄을 말로 전달해야 함
- 계속 바뀐다 — 배포는 하루 열 번, 방화벽 티켓은 그 속도를 못 따라감
핵심: 앱과 동떨어진 토폴로지 정의
- 핵심은 이 문장이다 — “네트워크 토폴로지 정의는 여전히 애플리케이션 자체와 동떨어져 있다.”
- 앱을 아는 사람(개발자)과 규칙을 정하는 사람(관리자)이 다른 사람이다
- 규칙이 다른 곳(방화벽 장비)에 산다
- DevOps로 두 팀이 친해져도 이 구조 자체는 안 바뀐다
이 장의 목표
- 평평한 기본 네트워크 위에, 앱을 아는 개발자가 앱과 함께 배포하는 방화벽을 겹겹이 세워, 파드 하나가 뚫려도 손이 밖으로 뻗지 못하게 하는 것
3. NetworkPolicy — L3/L4 방화벽
3.1 두 겹의 애플리케이션 방화벽 — L3/L4와 L7
네트워크판 shift left
- 쿠버네티스의 답은 23장에서 본 shift left의 네트워크판이다
- 개발 흐름을
기획 → 개발 → 테스트 → 배포 → 운영으로 그렸을 때- 맨 오른쪽(운영·관리자)에 있던 네트워크 규칙을 왼쪽(개발·개발자)으로 당겨온다
- 앱을 아는 사람과 규칙을 짜는 사람을 같은 사람으로 만드는 것이다
애플리케이션 방화벽
- 이때 만드는 것이 “애플리케이션 방화벽"이다
- 회사 방화벽 장비가 아니라 앱 하나하나가 자기 방화벽을 들고 다니는 그림이다
- 주문 서비스를 배포할 때 Deployment YAML 옆에 “나는 결제 서비스랑만 통신함” YAML을 같이 넣는다
- 앱이 어디로 옮겨가든 방화벽이 따라간다
구현 수단 두 가지
- 구현 수단은 둘이고, 경쟁 관계가 아니라 같이 쓰는 것이다
| NetworkPolicy | AuthorizationPolicy (서비스 메시) | |
|---|---|---|
| 어디 것 | 쿠버네티스 기본 내장 | Istio (따로 설치) |
| 보는 계층 | L3/L4 — IP, 포트 | L7 — HTTP 경로, 메서드, 신원 |
| 비유 | 봉투 겉면(주소·우편번호)만 보고 통과 | 봉투를 열어 내용을 보고 통과 |
| 누가 집행 | CNI 플러그인 (커널) | 사이드카 프록시 (파드 안) |
| “막아라” | 직접 못 적음 (허용만) | DENY 액션 있음 |
비유: 출입증과 세부 권한
- 건물로 치면 NetworkPolicy는 출입증 — 이 층에 들어올 수 있냐 없냐
- 서비스 메시는 층 안에서 어느 방을 열 수 있고 캐비닛은 보기만 되고 못 꺼내고 하는 세부 권한이다
- 둘 다 쓰면 출입증으로 1차 거르고, 안에서 2차로 거른다
- 책은 기본 내장이라 설치할 게 없는 NetworkPolicy부터 간다
3.2 NetworkPolicy — 종이는 쿠버네티스가, 집행은 CNI가
NetworkPolicy 리소스
- NetworkPolicy는 파드의 들어오는(ingress) 연결과 나가는(egress) 연결에 규칙을 거는 쿠버네티스 리소스다
- 그런데 이 장에서 가장 먼저 새겨야 할 사실이 하나 있다
NetworkPolicy는 규칙을 “적는 것"이고, 실제로 “막는 것"은 CNI 플러그인이다.
규칙을 적는 쪽과 막는 쪽
- 쿠버네티스 API 서버는 NetworkPolicy YAML을 받아 저장할 뿐이다
- 그걸 읽고 실제로 패킷을 떨어뜨리는 건 CNI(Container Network Interface) 플러그인이다
- 파드에 IP를 주고, 가상 네트워크 선을 꽂고, 노드 간 경로를 잡아주는 파드 네트워크 담당 부품
- 쿠버네티스 본체에는 안 들어 있고 골라서 깐다
- 아파트 관리사무소가 통신사를 불러 인터넷 선을 깔아주듯
- 그리고 모든 CNI가 NetworkPolicy를 지원하는 것은 아니다
| CNI | NetworkPolicy | 비고 |
|---|---|---|
| Flannel | ❌ | 연결만 해줌. 정책은 저장만 되고 아무것도 안 막힘 |
| Calico | ✅ | 가장 흔한 선택 |
| Cilium | ✅ | eBPF 기반, L7까지 (5.3) |
| EKS / GKE / AKS | ✅ | 기본이거나 옵션 |
| Minikube | ✅ | --cni=calico 등 |
함정: 성공이라고 뜨는 apply
- 이 장에서 가장 조심해야 할 함정이다
- CNI가 지원하지 않으면
kubectl apply는 성공이라고 뜬다- 에러도 경고도 없다
- 그런데 아무것도 안 막힌다
- 방화벽 걸었다고 안심하고 있는데 문이 그대로 열려 있는 상태다
- 그래서 정책을 처음 걸어보면 반드시
curl로 실제로 막히는지 직접 확인해야 한다- “YAML이 들어갔다"와 “동작한다"는 다른 문장이다
CNI 상태별 동작
| 상태 | 결과 |
|---|---|
| CNI가 아예 없음 | 파드가 IP를 못 받아 ContainerCreating에서 멈춤, 노드 NotReady (프리한 게 아니라 네트워크 자체가 없음 — kubeadm init 직후 그 상태) |
| NetworkPolicy 미지원 CNI | 다 연결됨, 아무것도 안 막힘 (chapter 처음의 flat network) |
| 지원 CNI | 다 연결됨, 정책 걸면 막힘 |
- 내 클러스터가 어느 줄인지는
kubectl get pods -n kube-system에 calico/cilium/flannel 중 뭐가 떠 있는지 보면 된다
3.3 해부 — podSelector, ingress, egress
NetworkPolicy의 두 부분
- 누구한테 걸 건가 →
spec.podSelector - 무슨 규칙인가 →
ingress/egress
책의 첫 예제
- “DB 파드에는 backend 파드만 들어올 수 있다.”
# Example 24-1. 인그레스 트래픽을 허용하는 간단한 NetworkPolicy
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: allow-database
spec:
podSelector: # ❶ 누구를 지킬 건가 → DB 파드
matchLabels:
app: chili-shop
id: database
ingress: # ❷ 들어오는 규칙 목록 (여러 개면 OR)
- from: # 이 규칙의 출발지 목록
- podSelector: # ❸ 누구를 들여보낼 건가 → backend 파드
matchLabels:
app: chili-shop
id: backend
위치가 뜻을 정하는 podSelector
podSelector가 두 번 나와서 헷갈리기 쉬운데, 위치가 뜻을 정한다
| 위치 | 역할 | 비유 |
|---|---|---|
spec.podSelector (바깥) | 이 정책이 보호하는 파드 | 문지기 세울 집 |
ingress[].from[].podSelector (안쪽) | 들여보낼 파드 | 출입 명단 |
이름 대신 라벨로 고르는 이유
- 파드 이름이
backend-7d9f8-abc12처럼 랜덤이고 계속 바뀌기 때문이다 - 라벨은 Deployment의
template.metadata.labels에 적어두면 거기서 나오는 파드마다 자동으로 붙는 이름표 스티커다- 파드가 죽고 새로 떠도 정책이 자동으로 따라붙는다
- Service가 파드를 찾는 그
selector와 완전히 같은 원리다
예제에 적혀 있지 않은 것
- 이 예제에서는 적혀 있지 않은 것이 중요하다
| 안 적은 것 | 결과 |
|---|---|
| frontend → DB | ❌ 막힘. 명단에 없으니까 — “deny"라고 안 적었는데도 |
| 포트 없음 | backend는 DB의 모든 포트 접근 가능. 좁히려면 ports: 추가 |
| 네임스페이스 없음 | 같은 네임스페이스의 backend만 (3.6) |
| egress 없음 | DB가 나가는 건 제한 없음. 이 정책은 들어오는 것만 건드림 |
핵심 철학: 허용 목록만 적는다
- 첫 줄이 NetworkPolicy의 핵심 철학이다 — “막아라"라고 적는 칸이 없다
- 허용 목록(allow list)만 적는다
- 파드에 정책 없음 → 다 열림
- 파드에 정책 하나라도 → 적힌 것만 열리고 나머지는 자동 차단
거부 규칙이 없는 이유
- 허용과 거부가 공존하면 충돌이 생긴다
- 정책 A는 frontend 허용, 정책 B는 frontend 거부 — 누가 이기나?
- 우선순위·순서를 정해야 하고, 정책이 많아질수록 “결국 열린 거야 닫힌 거야” 계산이 지옥이 된다
- 허용만 있으면 단순하다
- 모든 정책의 허용 목록을 합집합하면 끝이고 순서는 무관하다
- 대신 “다 열되 이거 하나만 막아"가 불편하다
- 그 자리를 뒤에서 Istio의
DENY가 채운다(4장)
- 그 자리를 뒤에서 Istio의
규칙의 재료
from(egress면to)에 올 수 있는 것은 셋이고, 옆에ports를 붙일 수 있다
| 재료 | 뜻 |
|---|---|
podSelector | 클러스터 안 파드 (라벨) |
namespaceSelector | 다른 네임스페이스 (라벨) |
ipBlock | 파드가 아닌 것 — 외부 서버, 노드, VPN 대역 (CIDR + except) |
ports | 그 규칙에 한해 “이 포트만” (from 과 AND) |
YAML의 -와 OR/AND
- YAML의
-는 “목록의 한 칸"이다ingress아래- from은 규칙 하나from아래- podSelector는 출발지 하나
- 규칙이 여러 개면 OR(하나라도 맞으면 통과)
- 한 규칙 안의
from과ports는 AND(둘 다 맞아야)다
3.4 라벨로 세그먼트 긋기 — 이름 직접 vs 역할 출입증
라벨: 의존성 그림을 YAML로
- 2.3에서 “개발자가 관리자에게 의존성을 말로 설명해야 한다"가 문제였다
- 라벨은 그 답이다 — 개발자 머릿속의 의존성 그림을 그대로 YAML로 옮긴다
라벨 두 개(app, id)를 쓰는 이유
- 둘 다 그냥 라벨이고 예약어가 아니지만, 저자가 용도를 나눠둔 것이다
| 라벨 | 뜻 | 비유 | 역할 |
|---|---|---|---|
app: chili-shop | 어느 앱 소속인가 | 아파트 이름 | 세그먼트 경계 |
id: database | 그 앱 안에서 뭐냐 | 호수 | 세그먼트 안의 역할 |
둘을 합쳐야 한 집이 나온다
id: backend만 쓰면 같은 네임스페이스의 다른 앱 backend가 딸려 들어온다app: chili-shop만 쓰면 frontend·backend·database가 다 잡혀 “backend만"을 못 고른다- 그래서 “래미안 101호"처럼 둘을 합쳐야 딱 한 집이 나온다
app: 앱 전체를 가리키는 가장 큰 단위
- 여기서
app은 컨테이너 하나가 아니라 앱 전체를 가리키는 가장 큰 단위다 - chili-shop의 파드가 9개면 9개 전부에 이 라벨이 붙는다
- 라벨은 파드에 붙고(컨테이너에는 없다), NetworkPolicy도 파드 단위로만 본다
라벨링 방식 두 가지
- 책은 라벨링 방식을 두 가지로 나눈다
방법 ① — 워크로드별 고유 라벨(이름 직접 부르기)
- 워크로드 하나당 라벨 값 하나
id: database는 database StatefulSet 전용이다- 다른 워크로드가 같은 값을 쓰면 안 된다(정책이 엉뚱한 데도 걸린다)
- 같은 워크로드에서 나온 파드 3개가 같은 값을 갖는 건 당연히 괜찮다 — 그게 “하나"다
방법 ② — 역할 라벨(출입증)
- 정책에 이름을 안 적고 역할을 적는다
# Example 24-2. 역할 기반 네트워크 세그먼트 정의
kind: Pod
metadata:
labels:
app: chili-shop
id: backend
role-database-client: 'true' # DB 출입증
role-cache-client: 'true' # 캐시 출입증
---
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: allow-database-client
spec:
podSelector:
matchLabels:
app: chili-shop
id: database
ingress:
- from:
- podSelector:
matchLabels:
app: chili-shop
role-database-client: 'true' # "DB 출입증 있는 애 누구든"
역할 라벨의 장점
- backend라는 이름이 정책 어디에도 안 나온다
- 나중에
reporting서비스가 DB를 써야 하면, reporting 파드에role-database-client: 'true'한 줄 붙이면 끝- 정책은 안 건드린다
'true'에 따옴표가 있는 건 라벨 값이 문자열이어야 해서다- 따옴표 없이
true는 YAML이 불리언으로 읽어 에러가 난다
- 따옴표 없이
- 키 이름 자체에 역할을 넣은 건 같은 키를 파드 하나에 두 번 못 달기 때문이다
두 방법 비교
| 방법 ① 이름 직접 | 방법 ② 역할 출입증 | |
|---|---|---|
| 새 워크로드 추가 | 정책 수정 필요 | 스티커만 붙임 |
| 정책 읽기 | 한눈에 — 정책 파일 하나로 “backend가 DB 쓰는구나” | 정책엔 “출입증 소지자"만 있어 누가 갖고 있는지 워크로드를 뒤져야 함 |
| 어울리는 상황 | 컴포넌트 적고 잘 안 바뀔 때 | 컴포넌트 많고 자주 늘 때 |
- 출입 명단에 “김철수, 이영희"가 적힌 것(①)과 “직원증 소지자"라고 적힌 것(②)의 차이다
- 저자는 ②가 유연하긴 하지만 ①이 읽기 쉬워 자주 낫다는 입장이다
3.5 deny-all로 시작하라 — 빈 리스트와 빈 규칙의 함정
자물쇠 안 단 파드
- 지금까지 한 것은 “DB 파드에 자물쇠 달기"다
- 자물쇠 안 단 파드는 그냥 열려 있다
| 파드 | 정책 | 상태 |
|---|---|---|
| database | 정책 있음 | 잠김 ✅ |
| backend | 정책 없음 | 열림 ❌ 아무나 들어옴 |
| frontend | 정책 없음 | 열림 ❌ |
원인: 기본값이 “다 열림”
- 파드 10개면 정책 10개를 다 만들어야 하고 하나 까먹으면 거기가 구멍이다
- 다음 달에 누가 새 서비스를 띄우며 정책을 안 만들면 또 구멍이다
- 원인은 **기본값이 “다 열림”**이라서다
- 안전한 쪽이 아니라 위험한 쪽이 기본이라, 잠그는 걸 하나하나 해야 하고 안 한 건 열려 있다
해법: 기본값 뒤집기
- 23장 3.5의 capability(“비워두면 0개가 아니라 후한 기본 세트”)와 정확히 같은 함정이다
- 답도 같다 — 기본값을 뒤집는다. 일단 다 막고 시작한다
# Example 24-3. 들어오는 트래픽 전체 거부
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: deny-all
spec:
podSelector: {} # ❶ 조건 없음 = 이 네임스페이스의 모든 파드
ingress: [] # ❷ 허용 명단이 텅 빔 = 아무도 못 들어옴
deny-all 위에 허용 정책 얹기
- 세 줄이 네임스페이스 전체를 잠근다
- 이 위에 24-1 같은 허용 정책을 얹으면, 정책은 합집합이므로 deny-all이 다 막아도 allow-database가 backend를 열어준다
- 빼먹은 파드는 막혀 있고(안전), 새로 만든 파드도 막혀 있어(안전) 필요하면 그때 연다
- 실수해도 “안 되네?” 하고 알아채지, 조용히 뚫려 있진 않다
함정: 괄호 하나 차이로 정반대
- 여기서 이 장의 두 번째 큰 함정이 나온다
| 적은 것 | 뜻 | 결과 |
|---|---|---|
ingress: [] | 규칙 0개 | 아무도 못 들어옴 |
ingress: [{}] | 규칙 1개인데 조건 없음 | 아무나 들어옴 |
ingress: 안 적음 | 규칙 0개 | 아무도 못 들어옴 |
{}는 “조건 없음"이라podSelector에서는 “전부 선택"이었다- 규칙 자리에
{}를 넣어도 “조건 없이 다 허용"이 되어[{}]는 allow-all이 된다
한계: 네임스페이스 범위
- NetworkPolicy는 네임스페이스 범위라 “클러스터 전체 기본 차단"을 한 방에 하는 방법이 기본 기능엔 없다
- 네임스페이스 20개면 deny-all을 20번 만들어야 한다
- 새 네임스페이스가 생길 때 빼먹으면 거기만 다시 flat이 된다
해법
- CNI 확장(Calico
GlobalNetworkPolicy, CiliumCiliumClusterwideNetworkPolicy) - 네임스페이스 생성 자동화에 deny-all 포함
- Kyverno/Gatekeeper로 “정책 없는 네임스페이스는 생성 거부”
- 쿠버네티스 쪽에서도
AdminNetworkPolicy라는 전역 정책 규격을 만들고 있다- 다만 CNI가 별도 지원해야 하는 단계다
- 이 아쉬움을 Istio가 어떻게 푸는지는 4장에서 본다
3.6 namespaceSelector — 비운 것과 안 적은 것은 정반대
모순처럼 보이는 두 문장
- 2.1에서 “다른 네임스페이스는 컨트롤 못 한다"고 했는데, 3.3의 재료 목록에는
namespaceSelector가 있다 - 모순처럼 보이지만 위치가 다르다
못 하는 것: 다른 네임스페이스 파드를 “지키기”
spec.podSelector는 내 네임스페이스 안에서만 찾는다- team-a 에 만든 정책으로 team-b 의 DB 를 잠글 수 없다
할 수 있는 것: 다른 네임스페이스에서 오는 걸 “허용/차단”
from/to안에namespaceSelector를 적는다
비유: 자물쇠와 출입 명단
- 우리 집 문에 자물쇠 다는 건 우리 집만 된다
- 우리 집 출입 명단에 옆집 사람 이름 올리는 건 된다
from:
- namespaceSelector: # monitoring 네임스페이스 "안의"
matchLabels:
team: monitoring
podSelector: # prometheus 파드만 (같은 - 항목 → AND)
matchLabels:
app: prometheus
들여쓰기 한 칸 차이: AND와 OR
-를 따로 붙이면 OR가 된다 — “monitoring NS 전부 또는 내 NS의 prometheus”- 들여쓰기 한 칸이 의미를 가르니 실제로 자주 실수하는 지점이다
Table 24-1을 규칙 두 개로
- 책의 Table 24-1은
podSelector와namespaceSelector의 여섯 가지 조합을 나열한다 - 외울 필요 없이 규칙 두 개로 환원된다
규칙 1: {} 는 “조건 없음 = 전부”
podSelector: {}→ 파드 전부namespaceSelector: {}→ 네임스페이스 전부
규칙 2: 안 적으면(—) 기본값이 다르다
namespaceSelector안 적음 → 내 네임스페이스만podSelector안 적음 → 파드 전부
비운 것과 안 적은 것
- 즉
namespaceSelector는 비운 것({})과 안 적은 것이 정반대다
# 내 네임스페이스의 backend만
from:
- podSelector: {matchLabels: {id: backend}}
# 모든 네임스페이스의 backend ← {} 하나 붙인 것만으로 범위가 클러스터 전체
from:
- namespaceSelector: {}
podSelector: {matchLabels: {id: backend}}
한 줄 요약
{}는 “다"고, 안 적으면 “여기"다- 실무에서 제일 조심할 것은
namespaceSelector: {}를 무심코 붙여 다른 팀 네임스페이스까지 열어버리는 일이다
3.7 egress — 안은 열고 밖은 닫고, policyTypes 함정
egress: ingress의 거울상
- egress는 ingress의 거울상이다 —
from이to로 바뀌고 나머지는 같다 - egress deny-all도 같은 방식이다 —
policyTypes: [Egress]에 egress 규칙을 안 적으면 나가는 연결이 다 막힌다 - 문제는 DNS까지 같이 막힌다는 것이라, DNS는 따로 열어줘야 한다
DNS 문제
- 파드가 curl backend 를 하면 제일 먼저 하는 일
- “backend 가 IP 몇 번이지?” 하고 DNS(kube-system 의 CoreDNS 파드)에 물어본다
- 나가는 걸 다 막으면 DNS 도 못 물어봐서, backend 가 열려 있어도 이름을 못 찾는다
같은 화살표를 두 번 적는 문제
- ingress로 “DB는 backend만 받아"를 이미 막았는데 egress까지 빡빡하게 하면 backend 쪽에도 “DB로 나가도 됨"을 또 적어야 한다
- 같은 화살표를 양쪽에서 두 번 적는 셈이다
- 그래서 저자의 절충안은 이렇다
문만 잠그면 되는 이유
- 문(ingress)만 잘 잠그면 안에서 돌아다니는 건 놔둬도 된다 — 어차피 목적지 문이 잠겨 있다
- 대신 밖으로 나가는 건 막아서, 들어오긴 했는데 아무 데도 못 가게 한다
# Example 24-4. 클러스터 내부로 나가는 것만 허용
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: egress-allow-internal-only
spec:
policyTypes: # ❶ "나는 egress 만 담당" — 이게 없으면 사고 (아래)
- Egress
podSelector: {} # ❷ 이 네임스페이스의 모든 파드
egress:
- to:
- namespaceSelector: {} # ❸ 모든 네임스페이스의 모든 "파드"
밖을 막은 방법
namespaceSelector: {}는 모든 네임스페이스의 모든 파드다- 클러스터 밖 서버(구글, 외부 API)는 파드가 아니라 어느 네임스페이스에도 없다
- 명단에 못 들어가고 자동으로 막힌다 — “deny"라고 안 쓰고 밖을 막았다
- DNS는 CoreDNS 파드니까 명단에 들어가 정상 동작한다
진짜 교훈: policyTypes
- 이 필드는 정책이 어느 방향을 담당하는지 선언하는 칸이다
- 안 적으면 쿠버네티스가 추측한다
- Ingress 는 “항상” 포함
- Egress 는
egress:섹션이 있을 때만 포함
비대칭이 만드는 사고
ingress:만 적으면 egress는 안 건드린다egress:만 적으면 Ingress까지 딸려 온다- 딸려 온 Ingress에 규칙이 없으니 빈 목록 = 다 막힘
- 나가는 것만 정리하려던 정책이 모든 파드의 들어오는 문을 잠가버린다
egress: 섹션과 정책의 종류
- “
egress:라고 명시했는데도 추측한다고?” — 그렇다 egress:섹션은 “나가는 규칙의 내용“이지 “이 정책의 종류“가 아니다- “그럼 ingress는 안 건드리는 거지?“는 별도 질문이고 그 답은
policyTypes에서만 한다 - 쿠버네티스 입장에서는 “정책이 걸렸다 = 일단 ingress는 잠근다"가 기본 태도다
- 원래 ingress 제한용으로 만들어졌고 egress는 나중에 붙었다
- 설계가 직관적이진 않으니 egress 정책이면
policyTypes: [Egress]를 반사적으로 붙이는 습관이 실전 해법이다
policyTypes | 실제 적용 |
|---|---|
| 안 적음 + ingress만 있음 | Ingress |
| 안 적음 + egress만 있음 | Ingress + Egress ← 함정 |
[Egress] | Egress만 |
[Ingress, Egress] | 둘 다 |
밖에 나가야 하는 파드만 따로 열기
- 밖은 막았으니 이제 밖에 나가야 하는 파드(결제 → 외부 결제 API, 알림 → 슬랙)에게만 문을 따로 연다
- 밖은 파드가 아니니 IP로 적는다
# Example 24-5. IP 대역으로 외부 접근 허용 (except 문법 예시)
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: allow-external-ips
spec:
policyTypes: [Egress] # ★ 책 예제에는 이 줄이 빠져 있다 — 그대로 쓰면 모든 파드의 ingress 가 잠긴다
podSelector: {}
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0 # 세상 모든 IP 로 나가도 되는데…
except:
- 192.168.0.0/16 # …이 대역(보통 사내망)은 빼고
- 172.23.42.0/24
책 예제와 실제 사용
- 책 예제는
except문법을 보여주려 범위를 넓게 잡은 것이다 - 실제 “결제 파드에 결제 API만"이라면
podSelector를 좁히고cidr: 203.0.113.10/32처럼 한 주소만 적는다
내부 egress까지 잠글 거면 절대 막으면 안 되는 둘
- 24-4보다 더 빡빡하게 “안에서도 필요한 데만” 가고 싶다면 최소 이 둘은 열어야 한다
| 뭐 | 왜 | 어떻게 |
|---|---|---|
| DNS | 안 열면 이름을 못 찾아 아무 데도 못 감 | kube-system NS로 53 UDP + TCP (응답이 크면 TCP로 넘어간다) |
| API 서버 | 오퍼레이터·컨트롤러가 쿠버네티스와 대화하려면 | 라벨이 없어 IP로 — kubectl get endpoints -n default kubernetes로 확인 후 ipBlock + 6443 |
- 일반 앱이면 DNS만 챙기면 되고, API 서버는 컨트롤러를 만들 때만 신경 쓰면 된다
Endpoints는 최근 버전에서EndpointSlice로 넘어가는 중이다kubectl get endpointslices -n default로도 같은 정보가 나온다
3.8 도구 — 녹화(Inspektor Gadget)와 감사(Cilium)
레시피를 복사해서 고쳐 쓰기
- 서비스 10개면 allow 정책 10개 + deny-all + egress + DNS 예외… YAML이 금방 수십 장이 된다
- 책의 첫 조언은 처음부터 다 짜려 하지 말고 레시피를 복사해서 고쳐 쓰라는 것이다
- GitHub의 Kubernetes Network Policy Recipes에 흔한 패턴이 YAML로 정리돼 있다
진짜 문제: 이미 돌아가는 시스템
- 진짜 문제는 이미 돌아가는 시스템에 정책을 나중에 끼울 때다
- 누가 누구를 부르는지 문서도 없고 만든 사람은 퇴사했다
- 여기서 두 종류의 도구가 등장한다
- 둘 다 23장 3.4에서 본 “기록 먼저, 강제는 나중에"(SELinux permissive → enforcing)의 네트워크판이다
eBPF: 커널 안의 CCTV
- eBPF는 리눅스 커널 안에 샌드박스된 미니 프로그램을 안전하게 꽂는 기술이다
- 모든 패킷은 커널을 지나므로 커널에서 보면 다 보인다
- 원래 커널에 뭘 넣는 건 실수 한 번에 서버가 뻗는 위험한 일이었다
- eBPF 프로그램은 들어가기 전에 검증기가 검사한다(무한 루프·이상한 메모리 접근 거부)
- 커널 안에서 돌면서도 커널을 못 죽인다
- 앱 코드를 안 건드리고 사이드카도 안 붙이고, 앱은 관찰당하는지도 모른다
- 도청기가 아니라 CCTV다
- 브라우저 확장 프로그램이 크롬 소스를 안 고치고 기능을 붙이듯, 리눅스 커널의 차세대 플러그인 아키텍처라 불린다
공통 경고: 안 돌린 경로는 기록에 없다
- 두 도구 모두에게 붙는 같은 경고 — 안 돌린 경로는 기록에 없다
- 한 달에 한 번 도는 정산 배치, 장애 때만 뜨는 복구 로직이 녹화 기간에 안 돌면 정책에서 빠진다
- 나중에 그게 돌 때 막혀서 터진다
- 그래서 책은 녹화 중에 통합 테스트를 전부 돌려 모든 경로를 한 번씩 밟게 하라고 한다
- 도구는 마법이 아니고, 뭘 돌리느냐가 결과를 정한다
4. AuthorizationPolicy — 봉투 안을 보는 방화벽
4.1 포트로는 부족하다 — 서비스 메시가 봉투를 여는 이유
포트 하나에 매달린 API
- NetworkPolicy로 “frontend는 backend 8080에 접근 가능"까지 했다
- 그런데 backend가 이런 API를 갖고 있다면?
| API | 누가 써도 되나 |
|---|---|
GET /orders | frontend 가 써도 됨 |
POST /orders | frontend 가 써도 됨 |
DELETE /orders/all | 관리자만 |
GET /admin/users | 관리자만 |
L7을 보려면 길목이 필요하다
- 8080 포트를 열면 이 넷이 다 열린다
- 포트 하나에 API 수십 개가 매달려 있는데 포트 단위로만 막으니 세밀하게 안 된다
- L7을 보려면 패킷 안을 열어 HTTP를 해석해야 한다
- CNI는 IP·포트만 보고 지나보내는 구조라 길목에 뭔가를 세워야 한다
- 그것이 서비스 메시다
서비스 메시: 공통 일을 앱 밖으로
- 서비스 메시는 암호화·재시도·로깅·권한처럼 모든 앱이 똑같이 해야 하는 일을 앱 밖으로 뺀다
- 공통 부품(사이드카 프록시, 주로 Envoy)이 대신하게 한다
- 앱은
http://backend로 보낼 뿐인데 실제로는 프록시가 가로채 처리한다 - 파드마다 프록시가 붙어 무거운 탓에, Istio ambient 모드나 Cilium처럼 노드당 프록시 하나·eBPF로 가로채는 방식이 최근 흐름이다
- 책은 Istio를 예로 들되 Istio 전체가 아니라 **CRD 하나 —
AuthorizationPolicy**만 본다
CRD와 필드
- CRD(CustomResourceDefinition)는 쿠버네티스에 새 리소스 종류를 등록하는 방법이다
- Istio를 깔면
AuthorizationPolicy,VirtualService같은 종류가 새로 생겨kubectl apply로 똑같이 쓸 수 있다 - 그 YAML 안의
source,operation은 CRD가 아니라 그 종류의 필드(칸)다- Pod의
containers,image가 별도 리소스가 아닌 것과 같다
- Pod의
4.2 신원은 어디서 오나 — ServiceAccount와 mTLS
신원과 두 검문소의 흐름
- 먼저 이 인가가 어떤 신원 위에서 도는지 흐름부터 보자
용어 정리
- **인증(authentication)**은 “너 누구야?”, **인가(authorization)**는 “너 이거 해도 돼?“이며 순서가 있다
- 인증서는 “나 frontend야"를 증명하는 위조 불가능한 디지털 신분증이다
- TLS는 서버만 신분증을 보여주는 암호화, mTLS(mutual)는 양쪽 다 보여주는 것이다
- ServiceAccount는 파드가 쓰는 계정(사람용 User와 별개)이고 그 이름이 신원에 박힌다
IP 추측 vs 인증서 확인
- NetworkPolicy는 “이 IP에서 왔으니 frontend겠지"라고 IP → 파드 → 라벨로 역추적한다(IP는 속일 수 있다)
- Istio는 인증서를 직접 보니 못 속인다
4.3 해부 — selector, action, rules
NetworkPolicy와 닮은 구조
- 리소스 구조는 NetworkPolicy와 놀랄 만큼 닮았다
- 대상 고르고(
selector) - 액션 정하고(
action) - 규칙 적는다(
rules)
- 대상 고르고(
# Example 24-6. Prometheus 가 모든 앱의 /metrics 만 긁어갈 수 있게
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: prometheus-scraper
namespace: istio-system # ❶ 여기 두면 "모든 네임스페이스"에 적용
spec:
selector: # ❷ 누구를 지킬 건가 (없으면 NS 전체)
matchLabels:
has-metrics: "true" # 역할 출입증 방식 (3.4 방법 ②)
action: ALLOW # ❸ 규칙에 맞으면 통과
rules:
- from:
- source:
namespaces: ["prometheus"] # ❹ 누가 — prometheus 네임스페이스에서
to:
- operation:
methods: ["GET"] # ❺ 뭘 — GET /metrics/* 만
paths: ["/metrics/*"]
NetworkPolicy로는 못 하던 것 셋
| 할 수 있는 것 | 설명 | |
|---|---|---|
| ① | 클러스터 전역 정책 | istio-system 은 특별 취급 — 거기 둔 정책은 모든 NS 에 자동 적용 (3.5 의 아쉬움 해결. 새 NS 가 생겨도 자동으로 잠긴다) |
| ② | HTTP 수준 필터 | methods, paths — “8080 은 열되 GET /metrics 만” |
| ③ | DENY 액션 | “막아라"를 직접 적을 수 있다 (아래) |
액션 네 가지
| 액션 | 뜻 |
|---|---|
ALLOW | 규칙에 맞으면 통과 |
DENY | 규칙에 맞으면 차단 |
AUDIT | 통과시키되 로그만 (Cilium 감사 모드와 같은 발상) |
CUSTOM | 외부 서버(OAuth 등)에 물어봄 |
4.4 DENY와 평가 순서 — CUSTOM → DENY → ALLOW
평가 순서: CUSTOM → DENY → ALLOW
DENY가 생기면 3.3에서 미룬 충돌 문제가 돌아온다- Istio는 우선순위를 못 박아 푼다 —
CUSTOM → DENY → ALLOW- DENY가 하나라도 걸리면 ALLOW가 있어도 막힌다
- 유연함을 얻는 대신 규칙 하나를 더 외우는 셈이다
규칙의 논리 구조
- 규칙의 논리 구조는 NetworkPolicy와 같다
- 규칙 여러 개는 OR
- 한 규칙 안의
from/to/when은 AND - 한 필드 안의 값 여러 개(
methods: [GET, POST])는 OR
- 책 본문의 “모든 규칙이 만족되어야 한다"는 규칙 하나 안의 조건을 말하려던 표현으로 보인다
- 규칙 목록 전체가 AND라면 규칙을 두 개 적는 순간 거의 아무것도 안 맞게 된다
여기도 deny-all이 먼저
- 당연히 여기도 deny-all이 먼저다
- 24-6은 허용만 적었으므로 기본이 다 열려 있으면 의미가 없다
# Example 24-7. 클러스터 전체 기본 거부
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: deny-all
namespace: istio-system
spec: {} # selector 없음(모든 파드) + action 기본 ALLOW + rules 없음(허용 0개) = 전부 거부
spec: {} 한 줄의 의미
spec: {}한 줄이 클러스터 전체를 잠근다- NetworkPolicy의
ingress: []와 같은 원리(“ALLOW인데 허용할 게 없음”)에istio-system의 전역 효과가 합쳐진 것이다 - 운영 중이라면
action: AUDIT으로 먼저 돌려 뭐가 막힐지 보고 진짜 deny-all로 바꾸는 게 안전하다
4.5 설정 문서라는 점, 그리고 두 검문소 디버깅
첫째: 실행되는 프로그램이 아니라 설정 문서
- 이 YAML은 실행되는 프로그램이 아니라 설정 문서다 — Deployment처럼 파드가 생기지 않는다
kubectl apply로 저장되면istiod가 감지해 각 사이드카에 규칙을 뿌린다istiod는 Istio의 컨트롤 플레인(설정 배포·인증서 발급)이고, 27장 Controller처럼 동작한다- 사이드카가 요청마다 검사한다
- 벽에 붙이는 공지문과 그걸 읽고 집행하는 경비원의 관계다
- NetworkPolicy도
kubectl apply → API 서버 → CNI가 읽어 커널 규칙으로 변환으로 같은 구조다
둘째: 사이드카 없는 파드는 검사 못 함
- 사이드카가 주입되지 않은 파드는 AuthorizationPolicy가 검사하지 못한다
- 그래서 NetworkPolicy deny-all이 밑에서 받쳐줘야 하며, 책이 “둘을 함께 쓰라"고 하는 이유다
증상으로 범인 가리기
- 두 검문소를 다 봐야 하니 디버깅은 까다롭지만, 증상으로 범인을 대충 가릴 수 있다
| 증상 | 범인 |
|---|---|
| 타임아웃, 연결 자체가 안 됨 | NetworkPolicy (커널에서 패킷 드롭) |
| 연결은 되는데 403 Forbidden | AuthorizationPolicy (사이드카가 응답) |
5. Discussion
5.1 RBAC과 헷갈리지 말 것 — 쿠버네티스 조작 권한 vs 앱끼리 통신 권한
두 군데서 나오는 “authorization”
- 쿠버네티스에서 “authorization"이 두 군데서 나오는데 완전히 다른 것이다
- 같은 클러스터의 두 상황으로 보자
RBAC vs AuthorizationPolicy
| RBAC | AuthorizationPolicy | |
|---|---|---|
| 요청이 어디로 | 쿠버네티스 API 서버 | 내 앱 파드 |
| 누구의 권한 | 사람, 도구, 오퍼레이터 | 파드끼리 |
| 비유 | 건물 관리 권한 — 누가 방을 만들고 부수나 | 방끼리 왕래 규칙 — 101호가 502호에 택배 놓고 올 수 있나 |
서로 안 만나는 두 권한
- 민수에게 RBAC 전권을 줘도 민수가 frontend 파드 안에서
DELETE /orders를 날리면 AuthorizationPolicy가 막는다 - AuthorizationPolicy가 아무리 빡빡해도
kubectl delete로 파드 자체를 지우는 건 RBAC 영역이다 - 관리사무소 권한이 있다고 남의 집 냉장고를 열 수 있는 게 아니다
- 입주민 규칙이 빡빡하다고 관리사무소가 방을 못 부수는 게 아니다
한 줄 정리
- RBAC = 쿠버네티스를 조작할 권한, AuthorizationPolicy = 앱끼리 통신할 권한
- 책이 “접근 제어(RBAC)는 주로 오퍼레이터에게 유용하다"고 덧붙이는 이유가 있다
- 오퍼레이터가 API 서버에 “내 CRD 변화 알려줘"를 계속 물어보기 때문이다
- 3.7에서 API 서버 IP를 열어줘야 한다고 한 것과 같은 맥락이다
5.2 케이블에서 SDN을 거쳐 쿠버네티스로 — 규칙을 짜는 사람이 바뀌었다
네트워크 역사의 세 번째 단계
- 책의 Discussion은 이 패턴을 네트워크 역사의 세 번째 단계로 놓는다
컨트롤 플레인과 데이터 플레인의 분리
- SDN의 핵심 아이디어는 컨트롤 플레인(어디로 보낼지 결정)과 데이터 플레인(실제로 보냄)의 분리다
- 이 장에서 본 모든 것에 그대로 들어 있다
- 관제탑은 비행기를 직접 안 몰고 지시만 하듯
- 정책을 읽고 규칙을 계산하는 쪽(istiod, Calico 컨트롤러)
- 실제로 패킷을 막는 쪽(사이드카, 커널의 iptables/eBPF)
- 그래서 정책을 바꿔도 트래픽은 계속 흐르고, 새 지시만 받으면 된다
덮어씌우기(overlay)
- “덮어씌운다(overlay)“는 표현도 정확하다 — 기본 네트워크는 건드리지 않고 여전히 flat이다
- 그 위에 정책이라는 층을 얹어 통과·차단을 정한다
- 정책을 다 지우면 다시 flat으로 돌아간다
- 23장에서 컨테이너가 “제대로 설정했을 때만” 울타리였듯, 쿠버네티스 네트워크도 정책을 얹었을 때만 세그먼트가 된다
5.3 eBPF와 Cilium — 두 겹을 하나로
두 개를 따로 쓰는 불편함
- 이 장의 불편함은 두 개를 따로 써야 한다는 것이었다
- YAML도 두 종류
- 집행 주체도 둘(CNI/사이드카)
- 디버깅도 두 군데
- 책의 마지막 문단은 그 불편함이 사라지는 방향을 가리킨다
# CiliumNetworkPolicy — 리소스 하나에 L3/L4 와 L7 을 같이 적는다
ingress:
- fromEndpoints:
- matchLabels:
id: frontend # ← L3/L4: 누가
toPorts:
- ports:
- port: "8080"
rules:
http:
- method: GET # ← L7: 뭘
path: /orders
가능한 이유: 커널이 곧 길목
- eBPF가 커널에서 패킷을 직접 검사할 수 있어서다
- L3/L4 규칙은 eBPF가 커널에서 바로 집행한다
- 4장에서 “L7을 보려면 길목에 프록시를 세워야 한다"고 했다
- Cilium도 L7(HTTP) 규칙은 프록시가 본다 — eBPF가 해당 트래픽을 노드마다 하나 있는 Envoy로 돌려보낸다
- 프록시가 파드마다가 아니라 노드마다 하나라서 사이드카를 안 붙여도 된다
- 리소스 하나, 관리하는 도구 하나
| 집행 주체 | L3/L4 | L7 | |
|---|---|---|---|
| 책의 방식 | CNI + Istio 사이드카 | NetworkPolicy | AuthorizationPolicy |
| Cilium | eBPF(L3/L4) + 노드당 Envoy(L7) | CiliumNetworkPolicy | CiliumNetworkPolicy |
전망과 지금
- 책이 쓰인 시점(2023) 기준으로 “앞으로의 쿠버네티스 버전에서"라는 전망이었다
- 지금은 Cilium이 꽤 표준에 가까워졌으며 Istio도 사이드카 없는 ambient 모드로 같은 방향을 가고 있다
- 다만 어느 도구를 쓰든 패턴 자체는 그대로다
- 기본은 다 뚫려 있다
- 앱을 아는 사람이 라벨로 세그먼트를 긋는다
- deny-all에서 시작해 필요한 문만 연다
핵심 메시지
Network Segmentation 의 몫: 뚫린 파드의 “손"이 밖으로 뻗지 못하게 하는 앱 방화벽
| 구분 | 내용 |
|---|---|
| 전제 | 네임스페이스는 폴더일 뿐 방화벽이 아니다. 기본은 flat, 다 뚫려 있다 |
| 1겹 | NetworkPolicy — podSelector 로 지킬 대상, ingress/egress 로 들일 대상. 허용 목록만 존재: 적힌 것만 열리고 나머지는 자동 차단. 집행은 CNI — Flannel 은 못 하고, 지원 안 되면 조용히 아무것도 안 막힌다 |
| 라벨 | app 으로 세그먼트 경계, id/role 로 역할. 이름 직접(읽기 쉬움) vs 출입증(유연함) |
| 기본값 | deny-all 을 깔고 필요한 문만 연다. [] 는 잠금, [{}] 는 개방 |
| egress | 클러스터 안은 열고 밖은 닫는다. DNS(53 UDP/TCP)는 항상 열어라 |
| 함정 | egress 정책엔 policyTypes: [Egress] 를 반사적으로. {} 는 “다”, 안 적으면 “여기” |
| 도구 | 녹화(Inspektor Gadget)·감사(Cilium) — 단, 안 돌린 경로는 기록에 없다 |
| 2겹 | AuthorizationPolicy — HTTP 메서드·경로·신원(ServiceAccount) 까지, DENY 가능. istio-system 에 두면 전역, spec: {} 한 줄로 클러스터 전체 deny-all |
| 구분 | RBAC 은 쿠버네티스를 조작할 권한, AuthorizationPolicy 는 앱끼리 통신할 권한 |
| 문화 | Shift Left — 방화벽 티켓 대신, 앱을 아는 개발자가 앱 옆에 YAML 한 장 |
Network Segmentation 은 “연결을 만드는 패턴이 아니라, 연결을 끊는 패턴” 이다. 기본으로 주어진 평평한 네트워크 위에, 앱을 아는 사람이 라벨로 선을 긋고, 일단 전부 막은 뒤 그림에 있는 화살표만 하나씩 열고, 봉투 겉면(IP·포트)과 봉투 속(HTTP·신원) 두 검문소를 겹쳐 세워라. 그러면 파드 하나가 뚫려도, 그 파드가 닿을 수 있는 곳은 원래 닿아야 했던 곳뿐이다.
6. References
"By default, a pod is non-isolated for ingress; all inbound connections are allowed."
policyTypes를 생략했을 때의 기본값 결정 규칙(3.7), podSelector와 namespaceSelector를 같은 항목에 둘 때와 따로 둘 때의 AND/OR 차이(3.6), 그리고 NetworkPolicy가 네트워크 플러그인(CNI)의 지원 없이는 아무 효과가 없다는 점(3.2)을 설명한다.selector·action·rules의 구조, 루트 네임스페이스(istio-system)에 둔 정책의 전역 적용, 그리고 CUSTOM → DENY → ALLOW의 평가 순서를 정의한다 — 4장의 근거다.policyTypes: [Egress]가 빠져 있다. 규칙 평가에 대한 "all of the rules must be satisfied"도 실제 Istio 동작(규칙 간 OR)과 다르다.