0. Contents

1. Overview

네임스페이스 cn4 — 정책을 넣기 전diagnostic Pod10.244.1.8client: diagnostictoolswgetpg-clientpg_isreadyweb Pod10.244.2.5app: webgo-web :8080/ · /healthz · /readyz · /datapostgres-010.244.2.7app: postgrespostgres :5432SELECT 1 에 답한다123① diagnostic → Service web :8080 (wget)② web → Service db :5432 (/data 가 SELECT 1)③ diagnostic → Service db :5432 직접 (pg_isready)pg = headless Service, StatefulSet 이름용 (postgres-0.pg)정책이 없으면 셋 다 된다 — 04-1 의 세 규칙대로 Pod 는 서로 닿는다5절에서 ③ 만 막고 ①② 는 남긴다. 6절에서 ① 의 경로 중 /healthz 만 막는다
경로가 있다는 것과 그 경로를 써도 된다는 것은 다른 문제다 — 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 /HelloHTTP 가 뜨는가
GET /healthzHealthyHTTP 처리가 살아 있는가. DB 는 안 본다
GET /readyzReadyHTTP 요청을 받을 준비가 됐는가. DB 는 안 본다
GET /dataDatabase Connected: SELECT 1 = 1db 에 접속해 인증하고 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 리소스와 레이블

리소스이름레이블역할
Poddiagnosticclient: diagnostictools(wget) + pg-client(pg_isready). 시험하는 쪽
Deploymentwebapp: webgo-web :8080
StatefulSetpostgresapp: postgrespostgres :5432. emptyDir 라 Pod 가 지워지면 데이터도 사라진다
Servicewebselector app: webdiagnostic 이 부르는 이름
Servicedbselector app: postgresweb 이 부르는 이름. ClusterIP 있음
Servicepgselector app: postgres, clusterIP: Noneheadless. StatefulSet 의 이름(postgres-0.pg)용
Secretdb-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

postgres Pod10.244.2.7정책의 대상podSelector 로 고른 Pod이 Pod 기준으로 방향을 읽는다ingress — 이 Pod 가 받는 연결web Poddiagnostic PodTCP 5432 로 시작점선 = 응답. 같은 연결의 일부egress — 이 Pod 가 시작하는 연결CoreDNS :53외부 API격리는 방향마다 따로 — 이 Pod 를 고르는 ingress 정책이 없으면 전부 받고, 하나라도 있으면 허용 목록만 받는다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 두 문과 합집합

두 문을 다 지나야 한다Aweb PodA 의 egress 문db:5432 가목록에 있어야통과B 의 ingress 문web 이목록에 있어야통과A 가 시작한 새 연결 → B한쪽만 막혀도 실패. 비격리면 그 문은 없는 것B 의 응답은 이 연결의 일부 — B 의 egress 문을 안 본다두 문을 지나도 앱 응답까지 보장은 아니다같은 Pod 같은 방향의 정책은 합쳐진다정책 1web 에서 5432정책 2monitoring 에서 9187정책 3 (좁힘)web 에서 5432 만ingress 문 = 합집합web → 5432monitoring → 9187정책 3 은 더할 뿐넓은 허용 뒤에 좁은 정책을 더해도 앞의 허용은 남는다표준 API 에는 앞선 허용을 뒤집는 deny 가 없다
정책은 문을 여는 목록이지 닫는 목록이 아니다 — 좁히려면 넓은 정책을 지워야 한다

두 문을 다 지나야 한다

  • A 가 B 로 새 연결을 시작하려면 A 의 egress 허용과 B 의 ingress 허용을 둘 다 만족해야 한다
A 의 egressB 의 ingressA 가 시작한 새 연결
허용허용정책상 허용
허용불허차단
불허허용차단
불허불허차단
  • 한쪽이 비격리면 그쪽 문은 없는 것으로 읽는다 — 제한이 추가되지 않는다
  • “정책상 허용” 은 앱 응답 성공까지 보장하지 않는다 — 주소·경로·수신 소켓·Service 백엔드·인증·앱 상태가 따로 맞아야 한다

허용은 합쳐진다

  • 같은 Pod 의 같은 방향에 정책이 여럿이면 허용 목록은 합집합이다
  • 넓은 허용을 넣은 뒤 더 좁은 정책을 하나 더 만들어도 앞의 넓은 허용은 남는다 — 좁히려면 넓은 정책을 지워야 한다
  • 표준 API 에는 순서나 우선순위로 앞선 허용을 뒤집는 deny 가 없다 — Cilium·Calico 의 확장 정책에는 있지만 따로 읽는다

3.4 빈 것 셋

podSelector: {}spec:podSelector: {}이 네임스페이스의 모든 Pod 가 대상빈 선택자 = 전부 고른다대상이지 허용이 아니다ingress: []spec:ingress: []허용 규칙이 하나도 없다열린 문이 없다 → 전부 차단✗ingress: [{}]spec:ingress:- {}조건 없는 규칙이 하나 있다무엇이든 맞는다 → 전부 허용✓모양이 비슷한 셋이 뜻이 다르다 — 빈 선택자는 '전부 대상', 빈 목록은 '허용 없음', 빈 규칙은 '전부 허용'
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

상대방(peer)을 고르는 세 기준podSelector같은 네임스페이스의 Pod 레이블namespaceSelectorNamespace 객체의 레이블 → 그 안의 모든 PodipBlockCIDR 범위. except 로 일부 제외이름으로 고르려면 자동 레이블을 쓴다kubernetes.io/metadata.name: kube-system같은 - 안이면 AND, 다른 - 면 OR하나의 - 에 둘 다from:- namespaceSelector:group: monitoringpodSelector:service: logscraperAND — monitoring 안의 logscraper 만- 가 둘from:- namespaceSelector:group: monitoring- podSelector:service: logscraperOR — monitoring 전부 또는 logscraper규칙 하나 안의 ports 와 from 은 함께 만족해야 한다peer 여럿 중 하나 + 포트 여럿 중 하나면 그 규칙에 일치 · 포트는 목적지 포트, 생략하면 TCP(matchLabels 는 줄여 적었다)
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 여러 네임스페이스를 잇는 예

ns frontendapp: frontendfrontend Podapp: frontendns store정책 store-apistore Pod 전부podSelector: {}ingress · egress 문in: frontend → TCP 8080out: downstream-1·2 → 8080out: kube-dns → 53ns downstream-1downstream-1 Podns downstream-2downstream-2 Podns kube-systemCoreDNS k8s-app=kube-dnsTCP 8080TCP 8080UDP·TCP 53downstream 이 격리됐다면store 의 egress 허용만으로는열리지 않는다 — 그쪽에도receive-from-storeingress 허용이 필요하다이름으로 downstream 을찾으려면 DNS egress 도열어야 한다NodeLocal DNSCache 면목적지가 다르다
정책 하나는 자기 네임스페이스의 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 결과

diagnostic10.244.1.8client: diagnosticweb10.244.2.5app: webpostgres-010.244.2.7app: postgres정책의 대상ingress 문app=web → TCP 5432 만egress 문[] — 새 연결 없음① wget web:8080/data② web → db:5432✓app=web → 통과, SELECT 1 = 1점선 = 응답. ② 연결의 일부라egress [] 에 안 걸린다③ pg_isready -h db — diagnostic 이 직접✗client=diagnostic 은 목록에 없다 → no responseAPI 에 정책 객체가 생겼다 ≠ 집행됐다 — 분산 적용이라 잠깐 뒤에, 새 연결로 시험한다port-forward 는 kubectl → API server → 노드의 다른 길이다 — 정책 증거로 쓰지 않는다
대상 Pod 에 문 둘이 생긴 것뿐이다 — web 과 diagnostic 의 egress 는 그대로라 이름 찾기도, web 으로 가는 길도 그대로다
요청 경로정책 전정책 후
diagnostic → db TCP 5432 (pg_isready)accepting connections새 연결 차단 — no response
diagnostic → web /data → web → dbSELECT 1 = 1SELECT 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 — 경로까지 보려면

diagnostic10.244.1.8client: diagnostic표준 NetworkPolicysrc: client=diagnosticdst: app=web TCP 8080여기까지 본다 (L3·L4)경로·메서드는 모른다CiliumNetworkPolicy — Envoy 가 HTTP 를 읽는다✓GET /Hello✓GET /dataSELECT 1 = 1✗GET /healthz403 Forbiddenweb Pod10.244.2.5app: webkubelet 의 probe노드에서 출발 — 출발지가 다르다/healthz 가 403 이어도 probe 는 계속 성공한다L3·L4 차단은 패킷을 버린다 → timeout. L7 거부는 프로토콜에 맞는 답을 준다 → 403timeout 만 보이면 경로 규칙이 아니라 L3·L4 허용·DNS·Envoy 상태부터 본다
같은 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 정책을 읽는 순서

대상1podSelector · 같은 ns방향2policyTypespeer3같은 - AND · 다른 - OR포트4목적지 포트 · TCP 기본반대쪽5상대 Pod 의 정책부수 의존6DNS · 관측 · 외부 API누구에게 붙는 정책인가 → 어느 방향을 격리하나 → 누구를 허용하나 → 어느 포트로 → 건너편 문은 열렸나 → 이름 찾기 같은 곁가지는 열었나
정책이 안 먹을 때 보는 순서도 같다 — 대상이 틀렸는지, 방향이 틀렸는지, 건너편이 막았는지
  • 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 로 바로잡았다.
→ kubernetes.io — Network Policies
→ kubernetes.io — NetworkPolicy API reference
→ kubernetes.io — Labels and Selectors
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 정책에도 적용되기 때문이다.
→ docs.cilium.io — Layer 7 Examples
→ docs.cilium.io — Identity-Aware and HTTP-Aware Policy Enforcement
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 의 패턴 문서에 있다.
→ postgresql.org — pg_isready
→ hub.docker.com — postgres (18: /var/lib/postgresql)
→ pkg.go.dev — net/http ServeMux patterns