0. Contents
1. Overview
경로가 있다는 것과 그 경로를 써도 된다는 것은 다른 문제다 — 04-1·04-2 가 경로를 만들었고, 이 글은 그중 어느 것을 열어 둘지 정한다
04-1·04-2 에서 이어지는 것
- 04-1 은 Pod 가 IP 를 얻고 노드를 넘어 닿는 길, 04-2 는 바뀌는 Pod 를 이름과 가상 IP 로 찾는 길이었다
- 그 길들은 전부 열려 있다 — 04-1 의 세 규칙대로 어느 Pod 든 어느 Pod 에나 닿는다
- 이 글은 “그중 어느 길을 열어 둘 것인가” 다. 그 답을 적는 객체가 NetworkPolicy 다
이 글의 순서
- 2 절 — 실습 앱 셋: diagnostic, web, postgres. 누가 누구에게 무엇을 보내는지 먼저 정한다
- 3 절 — NetworkPolicy 가 무엇인가: 방향, 두 문, 합집합, 빈 것 셋
- 4 절 — 규칙을 읽는 법: 레이블 선택자, peer 의 AND·OR, 포트, 여러 네임스페이스
- 5 절 — 실습: db 앞에 문을 달고 셋 중 하나만 막는다
- 6 절 — 표준이 못 보는 것: HTTP 경로는 Cilium 의 L7 이 본다
2. App — 실습 앱 셋
2.1 web 의 네 경로
| 경로 | 답 | 무엇을 검사하나 |
|---|---|---|
GET / | Hello | HTTP 가 뜨는가 |
GET /healthz | Healthy | HTTP 처리가 살아 있는가. DB 는 안 본다 |
GET /readyz | Ready | HTTP 요청을 받을 준비가 됐는가. DB 는 안 본다 |
GET /data | Database Connected: SELECT 1 = 1 | db 에 접속해 인증하고 SELECT 1 이 실제로 돌아오는가 |
/healthz·/readyz가 DB 를 안 보는 이유 — DB 가 죽었다고 web 이 재시작을 반복하면 안 된다. 04-1 6.4 절과 같은 이유다/data는 요청마다 DB 에 새로 연결하고 끊는다 — 정책을 바꾼 뒤 옛 연결이 재사용돼 결과가 섞이는 일을 막기 위해서다. 3 초 제한GET /{$}는 루트만 받는다 —/를 모든 하위 경로의 기본 처리기로 두면 없는 경로에서도 Hello 가 나와 검사가 틀어진다
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello") })
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Healthy") })
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Ready") })
mux.HandleFunc("GET /data", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
conn, err := pgx.Connect(ctx, dsn) // 요청마다 새 연결
if err != nil { http.Error(w, "Database unavailable", 503); return }
defer conn.Close(ctx)
var result int
if err = conn.QueryRow(ctx, "SELECT 1").Scan(&result); err != nil || result != 1 {
http.Error(w, "Database query failed", 503); return
}
fmt.Fprintln(w, "Database Connected: SELECT 1 = 1")
})
docker build -t go-web:cn4 .
kind load docker-image go-web:cn4 --name cn4 # 노드 셋에 이미지를 넣는다
- 이미지는 scratch 위에 실행 파일 하나다 — 셸이 없어
exec로 들어갈 수 없고, probe 도 exec 가 아니라 httpGet 이어야 한다 imagePullPolicy: Never— kind 에 넣은 이미지만 쓴다. 넣지 않았으면 Pod 가 뜨지 않는다
2.2 리소스와 레이블
| 리소스 | 이름 | 레이블 | 역할 |
|---|---|---|---|
| Pod | diagnostic | client: diagnostic | tools(wget) + pg-client(pg_isready). 시험하는 쪽 |
| Deployment | web | app: web | go-web :8080 |
| StatefulSet | postgres | app: postgres | postgres :5432. emptyDir 라 Pod 가 지워지면 데이터도 사라진다 |
| Service | web | selector app: web | diagnostic 이 부르는 이름 |
| Service | db | selector app: postgres | web 이 부르는 이름. ClusterIP 있음 |
| Service | pg | selector app: postgres, clusterIP: None | headless. StatefulSet 의 이름(postgres-0.pg)용 |
| Secret | db-login | 비밀번호와 접속 URL. 문서에 공개된 실습값 |
- 레이블 셋(
app: web,app: postgres,client: diagnostic)은 5 절의 정책이 그대로 쓴다 — 바꾸면 정책도 같이 바꾼다 - diagnostic 의 두 컨테이너는 같은 Pod IP 와 같은 정책 범위를 쓴다 — 04-1 3.1 절
- postgres 의 readiness 는
pg_isready -h 127.0.0.1이다 — 초기화 중 잠깐 뜨는 Unix 소켓 서버를 보고 너무 일찍 Ready 가 되는 일을 막는다
apiVersion: v1
kind: Service
metadata: {name: db}
spec:
selector: {app: postgres}
ports:
- {name: postgres, port: 5432, targetPort: 5432}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: web}
spec:
replicas: 1
selector:
matchLabels: {app: web}
template:
metadata:
labels: {app: web}
spec:
containers:
- name: web
image: go-web:cn4
imagePullPolicy: Never
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef: {name: db-login, key: url}
ports:
- {name: http, containerPort: 8080}
startupProbe: {httpGet: {path: /healthz, port: http}, periodSeconds: 2, failureThreshold: 30}
livenessProbe: {httpGet: {path: /healthz, port: http}, periodSeconds: 5}
readinessProbe: {httpGet: {path: /readyz, port: http}, periodSeconds: 3}
---
apiVersion: v1
kind: Pod
metadata:
name: diagnostic
labels: {client: diagnostic}
spec:
containers:
- {name: tools, image: busybox:1.37.0, command: [sleep, "86400"]}
- {name: pg-client, image: postgres:18.6-alpine, command: [sleep, "86400"]}
- 전체 workloads.yaml 에는 Secret, headless Service pg, StatefulSet postgres, Service web 이 더 있다 — 위는 레이블과 probe 가 보이는 부분만 적었다
- PostgreSQL 18 이미지는 데이터 볼륨 위치가
/var/lib/postgresql로 바뀌었다 — 옛 예제의/var/lib/postgresql/data를 그대로 복사하지 않는다
2.3 배포와 기준선
- 아래의
k는 04-2 2.2 절에서 정의한 셸 함수다 —k() { kubectl --context kind-cn4 "$@"; }. 즉k -n cn4 …는kubectl --context kind-cn4 -n cn4 …와 같다
k() { kubectl --context kind-cn4 "$@"; } # 새 터미널이면 다시 정의
k -n cn4 apply -f workloads.yaml
k -n cn4 rollout status statefulset/postgres --timeout=180s
k -n cn4 rollout status deployment/web --timeout=180s
k -n cn4 wait --for=condition=Ready pod/diagnostic --timeout=180s
k -n cn4 get pods,services -o wide
k -n cn4 exec diagnostic -c tools -- wget -qO- -T 5 http://web:8080/ # Hello
k -n cn4 exec diagnostic -c tools -- wget -qO- -T 5 http://web:8080/healthz # Healthy
k -n cn4 exec diagnostic -c tools -- wget -qO- -T 5 http://web:8080/data # Database Connected: SELECT 1 = 1
k -n cn4 exec diagnostic -c pg-client -- pg_isready -h db -p 5432 -U cn4 -d cn4 -t 3 # accepting connections
- 넷이 다 성공해야 기준선이다 — 정책을 넣기 전에 안 되는 것이 있으면 DNS, Service 의 endpoint, DB 초기화, Secret 값부터 본다 (04-2 7.1 절)
pg_isready의 성공은 “서버가 연결을 받을 준비가 됐다” 까지다 — 인증과 SQL 성공은/data가 본다. 둘은 다른 검사다
3. NetworkPolicy — 문을 여는 목록
3.1 경로와 허용은 다른 문제
- CNI 가 만든 것은 길이다 — 어느 Pod 든 어느 Pod 에나 닿는다
- NetworkPolicy 는 그 길 위에 “누가, 누구에게, 어느 프로토콜과 포트로” 를 적는 객체다 —
networking.k8s.io/v1 - 적는 것은 API server 고, 실제로 막고 여는 것은 정책을 지원하는 네트워크 구현이다 — 이 실습은 Cilium
- 그래서 API 가 YAML 을 받았다는 사실만으로 정책이 집행됐다고 보지 않는다 — 집행 여부는 새 연결로 시험한다
| 필드 | 읽는 법 |
|---|---|
metadata.namespace | 이 정책이 속한 네임스페이스 |
spec.podSelector | 정책이 붙는 Pod — 같은 네임스페이스 안에서 고른다 |
spec.policyTypes | 격리할 방향 — Ingress, Egress |
spec.ingress | 받아도 되는 상대와 목적지 포트 |
spec.egress | 보내도 되는 상대와 목적지 포트 |
- 정책을 읽는 첫 걸음은 대상 Pod 를 찾는 것이다 — 그 Pod 를 기준으로 방향을 읽는다
podSelector는 같은 네임스페이스만 고르지만, 규칙 안의 상대방(peer)은 다른 네임스페이스까지 가리킬 수 있다 — 4.2 절
3.2 방향 — 받으면 ingress, 시작하면 egress
받으면 ingress, 시작하면 egress — 책의 반대 표기는 틀렸고, Ingress API 객체와도 다른 말이다
- 방향은 대상 Pod 기준이다 — 그 Pod 가 받는 연결이 ingress, 그 Pod 가 시작하는 연결이 egress
- 격리는 방향마다 따로다. 대상 Pod 를 고르는 ingress 정책이 하나도 없으면 ingress 는 비격리라 전부 받는다. 하나라도 있으면 허용 목록만 받는다. egress 도 같은 식으로 따로 판단한다
- ingress 만 제한한 정책은 egress 를 건드리지 않는다 —
policyTypes에 적은 방향만 격리된다 - 허용된 연결의 응답은 그 연결의 일부다 — 서버가 답하도록 반대 방향 규칙을 따로 만들 필요가 보통 없다
- 책의 “받으면 egress, 내보내면 ingress” 는 반대다. 바깥에서 들어오는 HTTP 의 문인 Ingress API 객체와도 다른 말이다
3.3 두 문과 합집합
정책은 문을 여는 목록이지 닫는 목록이 아니다 — 좁히려면 넓은 정책을 지워야 한다
두 문을 다 지나야 한다
- A 가 B 로 새 연결을 시작하려면 A 의 egress 허용과 B 의 ingress 허용을 둘 다 만족해야 한다
| A 의 egress | B 의 ingress | A 가 시작한 새 연결 |
|---|---|---|
| 허용 | 허용 | 정책상 허용 |
| 허용 | 불허 | 차단 |
| 불허 | 허용 | 차단 |
| 불허 | 불허 | 차단 |
- 한쪽이 비격리면 그쪽 문은 없는 것으로 읽는다 — 제한이 추가되지 않는다
- “정책상 허용” 은 앱 응답 성공까지 보장하지 않는다 — 주소·경로·수신 소켓·Service 백엔드·인증·앱 상태가 따로 맞아야 한다
허용은 합쳐진다
- 같은 Pod 의 같은 방향에 정책이 여럿이면 허용 목록은 합집합이다
- 넓은 허용을 넣은 뒤 더 좁은 정책을 하나 더 만들어도 앞의 넓은 허용은 남는다 — 좁히려면 넓은 정책을 지워야 한다
- 표준 API 에는 순서나 우선순위로 앞선 허용을 뒤집는 deny 가 없다 — Cilium·Calico 의 확장 정책에는 있지만 따로 읽는다
3.4 빈 것 셋
default-deny 는 빈 선택자 + 빈 목록 둘의 조합이다 — 빈 규칙 하나를 넣는 순간 반대가 된다
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: store
spec:
podSelector: {} # 이 네임스페이스의 모든 Pod 가 대상
policyTypes:
- Ingress
- Egress
ingress: [] # 받아도 되는 상대 없음
egress: [] # 보내도 되는 상대 없음
- 설명용 예다 — cn4 실습에는 5 절의 정책만 넣는다
policyTypes를 생략하면 API 의 기본화 규칙이 방향을 정한다 — 읽기 쉽게 명시한다- 네임스페이스 기본 차단을 운영에 쓰려면 실제 의존성을 여는 정책을 같이 설계한다 — DNS, DB, 외부 API, 관측 수집기. 하나만 빠져도 그 길이 막힌다
3.5 정책이 하지 않는 것
- 암호화하지 않는다 — 정책이 없다는 것은 닿을 수 있는 범위가 넓다는 뜻이지, 남의 패킷을 볼 수 있다는 뜻이 아니다. 내용 보호는 TLS·mTLS 나 네트워크 구현의 별도 암호화가 한다
- 모든 트래픽을 다루지 않는다 — 중심은 TCP·UDP·SCTP 연결이다. 같은 Pod 안의 localhost, ARP, ICMP 전부를 통제하는 방화벽 규격이 아니다
- 구현마다 다른 것이 있다 — 노드에서 Pod 로 오는 통신, hostNetwork Pod 의 식별, NAT 전후 어느 주소로 평가하는지, 정책을 바꾸기 전에 맺은 연결의 처리. 그래서 정책 시험은 보통 Pod 에서 새 연결로 한다
- 레이블이 곧 경계다 — 레이블을 바꿀 수 있는 사람은 경계를 옮길 수 있다. 누가 워크로드와 레이블을 바꿀 수 있는지를 RBAC 과 admission 으로 같이 정한다
4. Rules — 규칙을 읽는 법
4.1 레이블 선택자
- 레이블은
metadata.labels의 키·값 쌍이다. 이름이 하나를 가리킨다면 레이블은 같은 성질을 가진 여럿을 고른다 - 선택자의 모양은 API 마다 다르다 — NetworkPolicy 의
podSelector는 LabelSelector(matchLabels + matchExpressions), Service 의spec.selector는 키·값 맵
matchLabels: # colour=purple 이면서 shape=square. 조건끼리 AND
colour: purple
shape: square
matchExpressions: # 같은 뜻을 표현식으로
- key: colour
operator: In
values: [purple]
- key: shape
operator: In
values: [square]
| 연산자 | 뜻 | values |
|---|---|---|
| In | 키가 있고 값이 목록 안 | 하나 이상 |
| NotIn | 키가 없거나 값이 목록 밖 | 하나 이상 |
| Exists | 키가 있음 | 넣지 않음 |
| DoesNotExist | 키가 없음 | 넣지 않음 |
- matchLabels 와 matchExpressions 를 같이 쓰면 전부 만족해야 한다
- NotIn 은 키가 아예 없는 것도 통과시킨다 — 키의 존재까지 요구하려면 Exists 를 하나 더 둔다
- 책의
NotExists는DoesNotExist가 맞고, “In·NotIn 에는 값이 없어도 된다” 는 반대다
k get pods -A -l 'colour=purple,shape=square'
k get pods -A -l 'colour notin (red,orange,yellow),shape'
- 빈 선택자의 뜻은 쓰이는 자리마다 읽는다 — NetworkPolicy 의
podSelector: {}는 그 네임스페이스의 모든 Pod 지만, 모든 API 의 생략된 selector 가 그렇지는 않다
4.2 peer 셋과 AND · OR
YAML 의 하이픈 하나가 AND 와 OR 을 가른다 — 들여쓰기 한 칸이 허용 범위를 바꾼다
ingress.from과egress.to에는 상대방(peer)을 나열한다. 고르는 기준이 셋이다podSelector만 — 정책과 같은 네임스페이스의 Pod 레이블namespaceSelector만 — Namespace 객체의 레이블로 네임스페이스를 고르고, 그 안의 모든 Pod- 둘을 같은 peer 에 — 고른 네임스페이스 안에서 다시 Pod 레이블을 본다 (AND)
namespaceSelector는 Pod 의 레이블이 아니라 Namespace 의 레이블을 본다 — 이름으로 고르려면 자동 레이블kubernetes.io/metadata.name을 쓴다- 하이픈이 갈라 놓는다 — 같은
-안의 두 선택자는 AND,-가 둘이면 OR
4.3 ipBlock 과 포트
from:
- ipBlock:
cidr: 10.0.0.0/24
except:
- 10.0.0.10/32 # 이 한 주소만 뺀다. IPv6 단일 주소는 /128
ipBlock은 CIDR 로 상대를 고른다. 같은 peer 안에 Pod·네임스페이스 선택자와 섞지 않는다 — 다른 peer 로 나열하면 OR- Pod IP 는 바뀌니 클러스터 안 앱의 신원은 레이블로 고른다 — ipBlock 은 바깥 주소용이다
- 바깥 주소를 허용할 때는 Service DNAT 나 egress SNAT 가 평가 전인지 후인지에 따라 실제로 비교되는 주소가 달라진다 — 구현 문서를 본다
- 규칙 하나 안의
ports와from/to는 함께 만족해야 한다. peer 여럿 중 하나, 포트 여럿 중 하나면 그 규칙에 맞는다 - 포트는 목적지 포트다. 프로토콜을 생략하면 TCP. 이름 있는 포트는 대상 Pod 의 포트 정의와 맞아야 하고,
endPort범위는 숫자 포트와 같이 쓰며 구현 지원을 본다
4.4 여러 네임스페이스를 잇는 예
정책 하나는 자기 네임스페이스의 Pod 만 고른다 — 건너편 네임스페이스의 문은 건너편 정책이 연다
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: store-api
namespace: store
spec:
podSelector: {} # store 의 모든 Pod
policyTypes: [Ingress, Egress]
ingress:
- from:
- namespaceSelector:
matchLabels: {app: frontend}
podSelector: # 같은 - 안 → frontend ns 의 frontend Pod (AND)
matchLabels: {app: frontend}
ports:
- {protocol: TCP, port: 8080}
egress:
- to:
- namespaceSelector:
matchLabels: {app: downstream-1}
podSelector:
matchLabels: {app: downstream-1}
- namespaceSelector: # - 가 둘 → downstream-1 또는 downstream-2 (OR)
matchLabels: {app: downstream-2}
podSelector:
matchLabels: {app: downstream-2}
ports:
- {protocol: TCP, port: 8080}
- to: # 이름으로 downstream 을 찾으려면 DNS 도 열어야 한다
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: kube-system}
podSelector:
matchLabels: {k8s-app: kube-dns}
ports:
- {protocol: UDP, port: 53}
- {protocol: TCP, port: 53}
- Namespace 객체에
app=frontend,app=store,app=downstream-1,app=downstream-2레이블이 있고 Pod 에도 같은 레이블이 있다는 전제다 — cn4 실습에는 넣지 않는다 - store 의 egress 허용만으로 downstream 의 문이 열리지는 않는다 — downstream-1 이 격리돼 있으면 그쪽에
receive-from-store같은 ingress 허용이 따로 필요하다. 3.3 의 두 문이다
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: receive-from-store
namespace: downstream-1
spec:
podSelector:
matchLabels: {app: downstream-1}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: {app: store}
ports:
- {protocol: TCP, port: 8080}
- DNS 조각을 모든 클러스터에 복사하지 않는다 — NodeLocal DNSCache 를 쓰면 Pod 가 먼저 묻는 주소가 다르다. 클라이언트의 resolv.conf, 실제 DNS Pod 의 레이블, 쓰는 정책 구현을 먼저 본다 (04-2 5.3 절)
5. Lab — db 앞에 문 달기
5.1 정책 db-from-web
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-from-web
spec:
podSelector:
matchLabels: {app: postgres} # 대상: postgres Pod
policyTypes: [Ingress, Egress] # 두 방향 다 격리
ingress:
- from:
- podSelector:
matchLabels: {app: web} # web 에서
ports:
- {protocol: TCP, port: 5432} # 5432 로 시작한 연결만
egress: [] # postgres 가 새로 시작하는 연결은 없음
- 3.1 의 순서로 읽으면 — 대상은
app: postgres, 방향은 둘 다, ingress 허용은app: web에서 TCP 5432 하나, egress 허용은 없음 - egress 가 비었어도 web 요청에 대한 응답은 간다 — 3.2 의 “응답은 그 연결의 일부”
k -n cn4 apply -f db-policy.yaml
k -n cn4 exec diagnostic -c pg-client -- pg_isready -h db -p 5432 -U cn4 -d cn4 -t 3 # no response, 종료 코드 ≠ 0
k -n cn4 exec diagnostic -c tools -- wget -qO- -T 5 http://web:8080/data # 여전히 SELECT 1 = 1
5.2 결과
대상 Pod 에 문 둘이 생긴 것뿐이다 — web 과 diagnostic 의 egress 는 그대로라 이름 찾기도, web 으로 가는 길도 그대로다
| 요청 경로 | 정책 전 | 정책 후 |
|---|---|---|
| diagnostic → db TCP 5432 (pg_isready) | accepting connections | 새 연결 차단 — no response |
diagnostic → web /data → web → db | SELECT 1 = 1 | SELECT 1 = 1 |
| db → web 으로 돌아가는 응답 | 허용 | 허용 — 같은 연결 |
- 바뀐 것은 postgres Pod 의 문 둘뿐이다 — web 과 diagnostic 의 egress 는 그대로라 이름 찾기(DNS)도, web 으로 가는 길도 그대로다
- 정책은 분산 적용이라 넣은 직후 잠깐 반영 시간이 있다 — 충분히 지난 뒤 새 요청으로 다시 본다
- web 의 egress 까지 제한하는 확장 실습이라면 db 의 TCP 5432 와 함께 DNS 의 UDP·TCP 53 을 열어야 한다 — 4.4 의 DNS 조각
5.3 port-forward 는 증거가 아니다
k -n cn4 port-forward deployment/web 18080:8080 # 브라우저에서 127.0.0.1:18080 — 끝나면 Ctrl+C
- 이 길은 kubectl → API server → 노드 → Pod 다. diagnostic Pod 가 Service 로 가는 보통의 길과 다르다
- 그래서 port-forward 가 되거나 안 되는 것을 정책 집행의 증거로 쓰지 않는다 — 정책 검증은 보통 Pod 에서 새 연결로 한다
6. L7 — 경로까지 보려면
같은 TCP 포트에 연결은 되는데 특정 요청만 거부된다 — 이것이 L7 이고, 표준 API 밖의 Cilium 전용 기능이다
- 표준 NetworkPolicy 는 HTTP 의 메서드나 경로를 모른다 — 보는 것은 출발지·목적지 레이블과 TCP 포트까지다
- Cilium 의 CiliumNetworkPolicy 는 Envoy 로 HTTP 를 읽는다 — 같은 TCP 포트에 연결은 되는데 특정 요청만 거부할 수 있다
- 이 실습에서 Cilium 전용인 부분은 여기뿐이다 — 표준 정책은 어느 CNI 에서나 같다
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: web-http-paths
spec:
endpointSelector:
matchLabels: {app: web} # 대상: web Pod (ingress 정책)
ingress:
- fromEndpoints:
- matchLabels: {client: diagnostic}
toPorts:
- ports:
- {port: "8080", protocol: TCP}
rules:
http:
- {method: GET, path: "^/$"} # 시작과 끝을 고정 — /data 와 /data/extra 를 구별
- {method: GET, path: "^/data$"}
k -n cn4 get networkpolicy,ciliumnetworkpolicy
k -n cn4 apply -f web-l7-policy.yaml
k -n cn4 exec diagnostic -c tools -- wget -qO- -T 5 http://web:8080/ # Hello
k -n cn4 exec diagnostic -c tools -- wget -qO- -T 5 http://web:8080/data # SELECT 1 = 1
k -n cn4 exec diagnostic -c tools -- wget -S -O- -T 5 http://web:8080/healthz # HTTP 403 Forbidden
- L3·L4 차단은 패킷을 버려서 timeout 이 난다. L7 거부는 프로토콜에 맞는 답(403)을 준다 — timeout 만 보이면 경로 규칙이 아니라 L3·L4 허용, DNS, Envoy 상태부터 본다
- 같은 출발지와 TCP 8080 을 L7 조건 없이 허용하는 규칙이 같이 있으면 경로 제한은 효과가 없다 — 3.3 의 합집합. 5 절의 db-from-web 은 postgres 만 고르니 겹치지 않는다
- kubelet 의 probe 는 노드에서 출발해 출발지가 다르다 —
/healthz가 diagnostic 에게 403 이어도 probe 는 계속 성공한다. 책은 web 의 egress 정책으로 설명했지만 실험 방향에 맞게 ingress 로 바꿨다
# 실습 정리 — 정책 둘만 지우고 워크로드는 그대로 둔다
kubectl --context kind-cn4 -n cn4 delete -f web-l7-policy.yaml
kubectl --context kind-cn4 -n cn4 delete -f db-policy.yaml
7. Discussion
7.1 정책을 읽는 순서
정책이 안 먹을 때 보는 순서도 같다 — 대상이 틀렸는지, 방향이 틀렸는지, 건너편이 막았는지
- DB 포트가 열렸다, 인증된 SQL 이 됐다, HTTP 경로가 허용됐다 — 셋은 다른 검사다.
pg_isready,/data,/healthz가 각각 하나씩 본다 - 정책의 대상과 방향을 실험 방향과 맞추고, 검사하려는 실제 경로에서 새 요청을 보내야 결과를 바르게 읽는다
- 04-2 7.1 의 진단표에 한 줄이 더해진 셈이다 — “양쪽 정책이 허용하나: 보내는 쪽 egress · 받는 쪽 ingress”
핵심 메시지
길과 허용은 다르다
CNI 가 길을 깔고, NetworkPolicy 가 그중 열어 둘 것을 적는다. 적는 건 API, 막는 건 네트워크 구현이다
받으면 ingress
대상 Pod 기준으로 받는 연결이 ingress, 시작하는 연결이 egress. 방향마다 따로 격리되고, 응답은 같은 연결이다
두 문, 합집합
보내는 쪽 egress 와 받는 쪽 ingress 를 둘 다 지나야 한다. 허용은 더해질 뿐이라 좁히려면 넓은 정책을 지운다
빈 것 셋
podSelector: {} 는 전부 대상, ingress: [] 는 전부 차단, ingress: [{}] 는 전부 허용. 하이픈 하나가 AND 와 OR 을 가른다
새 연결로 시험한다
객체가 생긴 것과 집행된 것은 다르다. port-forward 는 다른 길이고, 옛 연결은 재사용된다
표준은 포트까지
HTTP 경로는 L7 의 일이다. Cilium 의 Envoy 가 403 을 주고, L3·L4 차단은 timeout 으로 나타난다
8. References
정책이 네트워크 플러그인에 의해 집행된다는 것, 방향별 격리, 허용의 합집합, 빈 선택자·빈 목록·빈 규칙의 뜻, peer 의 세 기준과 AND·OR 는 NetworkPolicy 문서에 있다.
"Network policies are implemented by the network plugin. To use network policies, you must be using a networking solution which supports NetworkPolicy."
[해석] "네트워크 정책은 네트워크 플러그인이 구현한다. 정책을 쓰려면 NetworkPolicy 를 지원하는 네트워크 구현이 있어야 한다." — 3.1 의 "적는 것은 API, 막는 것은 구현" 과 "객체가 생긴 것 ≠ 집행된 것" 의 근거다. "Network policies do not conflict; they are additive" 가 3.3 의 합집합, 같은 문서의 namespaceSelector 와 podSelector 예가 4.2 의 AND·OR 의 근거다. 책의 ingress·egress 반대 표기와 NotExists 는 이 문서와 LabelSelector API 로 바로잡았다.
CiliumNetworkPolicy 의 endpointSelector · fromEndpoints · toPorts.rules.http, L7 거부가 403 으로 나타난다는 것, Envoy 프록시가 관여한다는 것은 Cilium 의 L7 정책 문서에 있다.
"Layer 7 policy rules are embedded into Layer 4 Example rules and can be specified for ingress and egress."
[해석] "L7 규칙은 L4 규칙 안에 들어가며 ingress 와 egress 양쪽에 쓸 수 있다." — 6 절의 YAML 모양(toPorts 안의 rules.http)의 근거다. 같은 출발지·포트를 L7 조건 없이 허용하는 규칙이 있으면 경로 제한이 무효가 되는 것은 표준 정책의 합집합 원리가 Cilium 정책에도 적용되기 때문이다.
PostgreSQL 18 이미지의 데이터 볼륨 위치 변경과 pg_isready 의 의미, Go 1.22 이후 ServeMux 의 메서드·경로 패턴(GET /{$})은 각 공식 문서에 있다.
"pg_isready is a utility for checking the connection status of a PostgreSQL database server."
[해석] "pg_isready 는 PostgreSQL 서버의 연결 상태를 확인하는 도구다." — 2.3 의 "연결을 받을 준비까지만 본다, 인증과 SQL 은 /data 가 본다" 의 근거다. 2.1 의 `GET /{$}` 가 루트만 받는다는 것은 net/http 의 패턴 문서에 있다.
