--- title: Calico NetworkPolicy 기반 DB 접근 통제 — 구성·검증·운영 가이드 date: 2026-08-18 type: guide domain: k8s tags: [calico, networkpolicy, globalnetworkset, globalnetworkpolicy, egress, database, iptables, ipset, kubernetes] --- > [!note] 원본은 스타일링된 HTML 가이드 + 배포용 매니페스트 > 이 글의 원문은 SVG 다이어그램 5종(트래픽 경로·평가 모델·참조 관계·판정 흐름·iptables 체인 워크)을 포함한 HTML 가이드다. 매니페스트는 GNS 2 + GNP 1 + 네임스페이스 + 클라이언트 2종 + 허용 NP를 **적용 순서대로** 담은 단일 파일이다. > > - **[→ 전문 보기 (calico-db-access-control.html)](files/calico-db-access-control.html)** > - **[→ 매니페스트 (db-access-control-manifests.yaml)](files/db-access-control-manifests.yaml)** > > 이 페이지는 같은 내용의 markdown 정리본(SVG는 ASCII로 대체). 선행 문서: [DB 연결은 egress 경유 제외](/docs/istio/egress/db-egress-exclusion/) — 그 결정문이 말하는 **통제 3층 중 ②층(Calico egress NetworkPolicy)의 구현 가이드**가 이 문서다. > [!key] 이 문서 전체를 관통하는 한 줄 > DB 대역으로 나가는 트래픽은 **order 500의 가드레일**에서 전부 막고, 그보다 **먼저 평가되는 order 200의 허용 정책**만이 문을 연다. ``` order 0 order 높음 | | +--[ 허용 NP · 200 ]------[ 차단 GNP · 500 ]------[ k8s NP · 1000 ]--> Allow rule1 Log kind:db knp.default.* db-set:mysql-pay rule2 Deny kind:db (Calico 자동 변환) tcp/3306 rule3 Allow (그 외) = DB에 관해 무력 ``` ## 0. 개요 — 전제·범위·용어 ### 0.1 환경 전제 - 온프레미스 Kubernetes(kubespray) + **Calico iptables 데이터플레인** + Istio 서비스 메시. - DB(MySQL·PostgreSQL·MongoDB·OpenSearch 등)는 **클러스터 외부**에 있고, 앱 pod의 DB 트래픽은 `excludeOutboundPorts`로 **sidecar를 거치지 않고 직결**됨. - DB 도메인의 DNS TTL은 1초(failover 즉시 반영용 사내 표준). Calico 정책은 **IP 기반**이라 DNS와 무관하게 동작하지만, failover로 바뀔 수 있는 IP를 전부 정책 대상에 포함해야 함(§7-2). - 이 문서의 리소스는 tier 기능 없이 **order만으로** 동작하므로 Calico v3.x 전 버전대에서 유효함. 단, §6의 flow log·Whisker·Staged policy는 v3.30+ 기능. ### 0.2 통제 목표 지정한 네임스페이스에서는 **DB 대역으로 나가는 egress를 기본 차단**하고, 명시적으로 허용 정책을 가진 네임스페이스 또는 pod만 **특정 DB 세트의 특정 포트**로 접근할 수 있게 한다. DB 외 트래픽(DNS, 사내 API, 메시 통신)에는 영향을 주지 않는다. 방화벽·DB 계정 통제는 그대로 유지되는 별도 계층이며, 이 문서는 그중 Calico 계층만 다룬다. ### 0.3 용어 — 첫 등장 정의 | 용어 | 정의 | |---|---| | **GNS** GlobalNetworkSet | 클러스터 밖 IP 대역(CIDR) 집합을 라벨 달린 전역 리소스로 등록해, 정책이 IP 대신 라벨로 참조하게 하는 Calico 리소스 | | **GNP** GlobalNetworkPolicy | 클러스터 전역 범위의 Calico 정책. 네임스페이스를 넘어 적용되며 Allow·Deny·Log·Pass 액션과 order를 가짐 | | **NP** (Calico) NetworkPolicy | `projectcalico.org/v3`의 네임스페이스 범위 정책. k8s 표준 NetworkPolicy와 **다른 리소스**이며 order·액션을 지원 | | WEP (WorkloadEndpoint) | Calico가 pod마다 만드는 엔드포인트 객체. pod의 veth 인터페이스 이름(`caliXXXX…`)을 여기서 확인 | | Felix | 각 노드의 calico-node 안에서 정책을 iptables 체인·ipset으로 컴파일해 커널에 넣는 에이전트 | | ipset | 커널의 IP 집합 자료구조. GNS의 CIDR 목록이 노드에서 `cali40s:` 형태의 ipset으로 실체화됨 | | conntrack | 커널 연결 추적 테이블. ESTABLISHED로 기록된 기존 연결은 정책 변경 후에도 유지됨(§7-3) | | SNAT / natOutgoing | pod가 클러스터 밖으로 나갈 때 소스 IP를 노드 IP로 바꾸는 동작. Calico IPPool의 `natOutgoing` 설정에 따름 | | implicit deny (암묵적 차단) | 어떤 방향(예: egress)에 대해 정책이 하나라도 pod를 select하는 순간, 그 방향의 기본값이 허용→차단으로 뒤집히는 Calico/k8s 공통 규칙 | | first-match 종결 | order 순서로 정책·룰을 훑다가 처음 매칭된 Allow/Deny에서 평가가 끝나는 규칙. Log는 기록만 하고 다음 룰로 계속 진행(비종결) | *표는 색인이다. 각 개념이 실제로 어떻게 동작하는지는 §1과 §5에서 확인 커맨드와 함께 다룬다.* ## 1. 배경 개념 3가지 필요한 개념은 정확히 셋: ① 정책이 **어느 지점**에 적용되는가, ② 여러 정책이 **어떤 순서로 평가**되는가, ③ 세 리소스가 **서로를 어떻게 참조**하는가. ### 1.1 정책 적용 지점 — pod의 veth ``` +---------------------+ +-------------------+ +---------------+ | App Pod | | veth caliXXXX | | Worker Node | | app + istio-proxy |--->| * POLICY POINT |--->| SNAT when | | DB port bypasses | | egress: cali-fw-*| | natOutgoing | | sidecar (exclude) | | ingress: cali-tw-* | = true | +---------------------+ +-------------------+ +-------+-------+ | +-------------+ +-------------------+ | | DB Zone | <---- | Zone Firewall | <-----+ | VIP + FO IP | | node CIDR -> | | TTL = 1s | | DB VIP:port | +-------------+ +-------------------+ ``` **그림 1** — Calico 정책은 pod의 veth를 지나는 시점, 즉 **SNAT보다 앞에서** 적용된다. sidecar를 우회하는 DB 트래픽도 veth는 반드시 지나므로 Calico 통제에는 영향이 없다. 반대로 방화벽은 SNAT 이후라 노드 IP만 보게 되어 워크로드를 구분하지 못한다 — **두 계층의 granularity가 다른 이유**다. ### 1.2 평가 모델 — order, first-match, 기본값 반전 Calico는 어떤 pod의 어떤 방향(egress)에 대해, 그 pod를 select하는 모든 정책을 **order 오름차순**으로 정렬해 훑는다. 각 정책 안에서는 룰을 위에서 아래로 평가하고, **처음 매칭된 Allow 또는 Deny에서 즉시 종결**한다. Log는 기록만 남기고 다음 룰로 계속 간다. 여기에 k8s 호환을 위한 규칙이 하나 얹힌다. **그 방향에 대해 정책이 하나도 pod를 select하지 않으면 전부 허용**(기본 허용)이지만, **하나라도 select하는 순간 기본이 차단으로 뒤집혀**, 끝까지 아무 룰에도 매칭되지 않은 트래픽은 버려진다(implicit deny). 이 반전 규칙이 §2.2에서 차단 정책 마지막에 무조건 Allow 룰을 두는 이유이고, §7-1 함정(허용 NP를 라벨보다 먼저 만들면 장애)의 원인이다. ``` 종결 규칙 기본값 규칙 +-----------------------------+ +----------------------------------------+ | first match Allow/Deny | | select 하는 정책 0개 -> 기본 허용 | | -> 평가 종료 | | 1개 이상 + 끝까지 무매칭 -> implicit | | Log 는 비종결 — 기록 후 계속 | | deny (차단) | +-----------------------------+ +----------------------------------------+ ``` **그림 2** — 허용(200)이 차단(500)보다 **먼저** 평가되기 때문에 "차단 속의 예외 허용"이 성립한다. k8s 표준 NetworkPolicy는 Calico가 `knp.default.*` 이름·order 1000으로 자동 변환해 같은 축에서 평가한다 — 500의 Deny 뒤에 있으므로 **k8s NP로는 이 설계의 DB 차단을 열 수 없다**(§2.4). ### 1.3 세 리소스의 역할 분담과 참조 관계 공식 문서의 외부 대역 통제 패턴은 "IP 목록"과 "정책 로직"을 분리한다. GNS가 DB 대역을 라벨 달린 객체로 만들고, 정책들은 IP가 아니라 **라벨 셀렉터**로 그 객체를 가리킨다. DB IP가 바뀌면 GNS만 고치면 되고, 정책 본문은 손대지 않는다. ``` GNS (라벨을 붙이는 쪽) 정책 (라벨로 가리키는 쪽) +----------------------+ kind +--------------------------------------+ | dbzone-all |<----------| GNP deny-db-egress order 500 | | kind = db | == "db" | nsSelector: db-egress-control | | nets: 10.20.0.0/16 | ^ | == "enabled" | +----------------------+ | | dest.selector: kind == "db" | | +--------------------------------------+ +----------------------+ | | dbset-mysql-pay |<----+ +--------------------------------------+ | kind = db | db-set | NP allow-db-mysql-pay order 200 | | db-set = mysql-pay |<----------| selector: app == "db-client-allowed" | | nets: .11/32 .12/32 | ==mysql- | dest.selector: db-set == "mysql-pay" | +----------------------+ pay | ports: [3306] | +--------------------------------------+ ``` **그림 3** — 차단은 넓은 라벨(`kind: db`)로 존 전체와 모든 세트를 매칭하고, 허용은 좁은 라벨(`db-set`)로 딱 한 세트만 매칭한다. **"차단은 넓게, 허용은 좁게"** 가 라벨 설계의 원칙이다. ## 2. 리소스 구성과 YAML 공식 문서의 외부 대역 통제 패턴(External IPs/networks rules in policy)을 이 환경에 맞춘 구성이다. 아래 YAML(GNS 2, GNP 1, 허용 NP)이 통제의 전체다. 첨부한 [`db-access-control-manifests.yaml`](files/db-access-control-manifests.yaml) 한 파일에 여기의 리소스 전부와 테스트 네임스페이스·클라이언트 2종까지 **적용 순서대로** 들어 있어 그대로 배포하면 §5의 검증까지 이어진다. 운영 적용 시에는 GNS·GNP를 플랫폼 리포로, Namespace·허용 NP를 앱 리포로 분리한다(§4-6단계). ### 2.1 GNS — DB 대역 정의 ```yaml # (a) DB 존 전체 — "차단의 그물". 아직 세트로 등록 안 된 DB까지 막는다. apiVersion: projectcalico.org/v3 kind: GlobalNetworkSet metadata: name: dbzone-all labels: kind: db spec: nets: - 10.20.0.0/16 # DB 존 서브넷 (사내 대역으로 치환) --- # (b) DB 세트 — "허용의 단위". failover로 이동 가능한 IP 전체를 한 객체로. apiVersion: projectcalico.org/v3 kind: GlobalNetworkSet metadata: name: dbset-mysql-pay labels: kind: db db-set: mysql-pay spec: nets: - 10.20.30.11/32 # 현재 primary VIP - 10.20.30.12/32 # failover 후보 — 전부 등록해야 함(§7-2) ``` | 필드 | 값 | 역할 | |---|---|---| | `metadata.labels.kind` | `db` | 차단 정책이 매칭하는 넓은 라벨. 존 GNS·세트 GNS 모두에 붙임 | | `metadata.labels.db-set` | `mysql-pay` | 허용 정책이 매칭하는 좁은 라벨. **세트 GNS에만** 붙임 | | `spec.nets` | CIDR 목록 | 이 집합이 대표하는 실제 IP 대역. 노드에서 ipset으로 실체화됨(§5.2) | **왜 존 GNS와 세트 GNS를 둘 다 두는가.** 차단이 세트 GNS에만 의존하면, 아직 등록하지 않은 신규 DB나 누락된 IP는 차단 대상에서 빠져 "모르는 DB는 열려 있는" 상태가 된다. 존 대역 전체를 `kind: db`로 함께 등록해 두면 차단의 그물이 서브넷 단위로 깔리고, 세트 GNS는 그 안에서 허용의 정밀 단위로만 쓰인다. **누락의 실패 방향이 "의도치 않은 허용"이 아니라 "의도치 않은 차단"이 되도록** 방향을 잡는 것이다. **세트의 등록 단위는 "현재 IP"가 아니라 "failover 집합".** 이 환경의 DB 도메인은 TTL 1초로 failover 시 즉시 다른 IP를 가리킨다. Calico는 IP만 보므로, 세트 GNS에 primary만 넣어두면 failover 순간 허용이 깨져 **통제 장치가 장애를 만든다**. DBA에게 "이 서비스가 가리킬 수 있는 IP 전체 목록"을 받아 등록하고, IP 추가·변경 절차를 GNS 수정 PR과 묶는다(§4, §7-2). ### 2.2 GNP — 지정 네임스페이스의 DB 대역 차단 ```yaml apiVersion: projectcalico.org/v3 kind: GlobalNetworkPolicy metadata: name: deny-db-egress spec: order: 500 namespaceSelector: db-egress-control == "enabled" # ← 네임스페이스 롤아웃 스위치 selector: all() # 대상 ns 안의 모든 pod types: - Egress egress: # rule 1 — 차단될 트래픽을 커널 로그에 기록 (비종결: 기록 후 다음 룰로) - action: Log destination: selector: kind == "db" && !has(projectcalico.org/namespace) # rule 2 — DB 대역으로 가는 egress 차단 (DROP) - action: Deny destination: selector: kind == "db" && !has(projectcalico.org/namespace) # rule 3 — DB 외 트래픽은 이 정책이 관여하지 않음. 이 줄이 없으면 # 기본값 반전(§1.2) 때문에 대상 ns의 "모든" egress가 막힌다. - action: Allow ``` | 필드 / 룰 | 역할 | |---|---| | `order: 500` | 허용 NP(200)보다 뒤, k8s NP(1000)보다 앞. **이 상대 위치가 설계의 뼈대** | | `namespaceSelector` | 라벨 붙은 네임스페이스에만 발동. 네임스페이스에 라벨을 붙이는 행위가 곧 "이 ns를 통제 대상에 편입" — 점진 롤아웃 스위치 | | `selector: all()` | 대상 ns 안 모든 pod. namespaceSelector가 이미 범위를 좁혔으므로 전역 `all()`이 아님 | | `types: [Egress]` | 나가는 방향만 통제. ingress는 이 설계 범위 밖이라 건드리지 않음 | | rule 1 `Log` | rule 2와 같은 매칭 조건으로 한 줄 앞에 둔다. Log는 비종결이라 기록 후 rule 2로 넘어가 차단됨 — "차단되는 모든 시도"가 노드 커널 로그에 남음(§6.1) | | rule 2 `Deny` | 목적지가 `kind: db` 라벨의 전역 리소스(=GNS)에 등록된 IP면 DROP. **프로토콜 제한을 두지 않아** TCP 외 ICMP 등도 함께 막힘 — ping으로는 검증할 수 없고(§5.4), 통제 표면이 단순해짐 | | rule 3 `Allow` | 무조건 허용으로 정책을 닫음. DB로 안 가는 트래픽(DNS·사내 API·메시)이 implicit deny에 빠지지 않게 하는 **안전핀** | **`!has(projectcalico.org/namespace)` 가드의 의미.** 네임스페이스에 속한 모든 리소스(pod, 네임스페이스 NetworkSet)에는 Calico가 `projectcalico.org/namespace` 라벨을 자동으로 붙인다. 이 라벨이 **없는** 대상은 전역 리소스, 즉 GlobalNetworkSet뿐이다. 가드를 빼면, 어떤 pod가 우연히(또는 악의로) `kind: db` 라벨을 달았을 때 그 pod로 가는 트래픽까지 rule 2에 걸려 차단된다. 공식 문서의 external-ips 예제가 쓰는 표준 가드다. > [!warning] rule 3을 지우면 생기는 일 > 대상 네임스페이스의 pod는 이 GNP에 select되어 egress 기본값이 차단으로 뒤집힌 상태다. rule 3이 없으면 DB 외 트래픽이 어떤 룰에도 매칭되지 않은 채 평가가 끝나고 — DNS를 포함한 **모든 egress가 조용히 버려진다**. 정책 리뷰 시 "마지막 Allow가 있는가"를 체크리스트 1번으로 둘 것. **공식 문서의 등가 변형.** external-ips 문서는 낮은 order의 Deny 정책 + 높은 order의 Allow 전체 정책, 두 개로 나누는 형태도 보여준다. 의미는 같지만 객체가 둘로 갈라지면 "Allow 쪽만 지워져 전면 차단이 되는" **반쪽 삭제 사고면**이 생긴다. 한 정책 안의 룰 3개로 담는 위 형태가 운영 실수 표면이 더 작아 이를 기본으로 한다. **namespaceSelector 변형 — 이름 지정·혼합·opt-out.** 이 필드는 등호 하나가 아니라 셀렉터 표현식 전체(`has()`·`!has()`·`in`·`not in`·`&&`·`||`)를 받는다. "지정한 네임스페이스 또는 라벨 붙은 네임스페이스"라는 요구는 아래 ②혼합형이 그대로 표현한다. ```yaml # ① 이름 지정 — 모든 ns에 자동 부여되는 불변 라벨(kubernetes.io/metadata.name)로 지목 namespaceSelector: kubernetes.io/metadata.name in {"calico-egress-policy-test"} # ② 혼합 — 라벨 붙은 ns "또는" 이름으로 지정한 ns namespaceSelector: db-egress-control == "enabled" || kubernetes.io/metadata.name in {"calico-egress-policy-test"} # ③ opt-out(최종 상태 권장) — 전 ns 기본 적용 + 명시 면제. 실패 방향이 fail-closed로 바뀐다 namespaceSelector: !has(db-egress-exempt) && kubernetes.io/metadata.name not in {"kube-system", "kube-public", "kube-node-lease", "calico-system", "calico-apiserver", "tigera-operator", "istio-system", "monitoring", "argocd"} ``` 변형을 쓸 때는 세 가지를 함께 규약화한다. 1. 부정형 연산자(`!has`·`not in`·`!=`)는 **라벨이 없는 네임스페이스도 매칭**한다 — opt-out이 성립하는 근거이자, `!= "enabled"` 같은 식을 무심코 썼을 때 전 클러스터를 쓸어 담는 이유다. 2. opt-out에서는 `db-egress-exempt` 라벨 부여가 곧 통제 해제 승인이므로, ns 라벨 수정 권한을 RBAC·Kyverno로 묶는다(§7-7과 같은 논리). hostNetwork pod(kube-proxy·calico-node 등)는 WEP이 아니라 애초에 정책 대상 밖이며, 제외 목록이 실제로 보호하는 것은 **coredns처럼 pod 네트워크를 쓰는 시스템 컴포넌트**다. 3. 권장 경로: opt-in으로 편입을 끝낸 뒤 `kubectl get ns -l '!db-egress-control'`로 잔여(통제 밖) ns가 없음을 확인하고, staged 관측(§4-3단계)을 거쳐 셀렉터만 ③으로 교체한다. opt-out이 되면 모든 ns가 항상 GNP 아래에 있어 **§7-1의 "허용 NP 단독 존재" 상태가 구조적으로 불가능**해진다. ### 2.3 NP — 네임스페이스/파드 단위 허용 ```yaml # 변형 A — 특정 pod만 허용 (권장: 최소 범위) apiVersion: projectcalico.org/v3 kind: NetworkPolicy metadata: name: allow-db-mysql-pay namespace: calico-egress-policy-test spec: order: 200 # 차단 GNP(500)보다 반드시 앞 selector: app == "db-client-allowed" types: - Egress egress: - action: Allow protocol: TCP destination: namespaceSelector: global() # 전역 리소스(GNS)를 평가 대상으로 selector: db-set == "mysql-pay" ports: - 3306 --- # 변형 B — 네임스페이스 전체 허용 (selector만 다름) # spec.selector: all() ← 해당 ns의 모든 pod에게 mysql-pay:3306 개방 ``` | 필드 | 역할 | |---|---| | `order: 200` | 500보다 앞이라 먼저 평가되고, Allow는 종결이므로 뒤의 차단에 닿지 않음. **600으로 바꾸면** rule 2 Deny가 먼저 매칭되어 이 정책은 영원히 평가되지 않음 — 허용이 "안 먹히는" 사고의 제1 원인 | | `spec.selector` | 허용 범위. `app == "db-client-allowed"`(pod 단위) 또는 `all()`(ns 단위). 운영에서는 앱 라벨(`app=pay-api` 등)로 치환. pod 단위를 기본으로 하고 ns 단위는 배치·잡이 많아 pod 열거가 무의미할 때만 | | `namespaceSelector: global()` | 네임스페이스 정책의 destination 셀렉터는 기본적으로 같은 ns 안만 봄. `global()` 연산자가 평가 범위를 전역 리소스(GNS)로 돌려, ns 정책이 GlobalNetworkSet을 참조할 수 있게 함. §2.2의 `!has(…)`와 **같은 문제를 반대 방향에서** 푸는 장치 | | `ports: [3306]` | 세트뿐 아니라 포트까지 좁힘. 같은 IP의 다른 서비스 포트(관리 포트 등)는 열리지 않음 | **k8s 표준 NetworkPolicy로는 안 되는 이유 두 가지.** 첫째, k8s NP에는 order가 없고 Calico가 order 1000으로 고정 변환하므로 500의 Deny를 앞설 수 없다. 둘째, k8s NP의 `to:`에는 GNS 라벨 참조가 없어 `ipBlock`으로 IP를 다시 하드코딩해야 한다 — GNS로 IP와 정책을 분리한 의미가 사라진다. **이 설계에서 DB 접근을 열 수 있는 유일한 리소스는 order<500의 Calico NP**이고, 따라서 이 리소스의 생성 권한이 곧 DB 접근 승인 권한이다(§7-7 거버넌스). > [!warning] 허용 NP를 "자립"시키려 하지 말 것 > 허용 NP의 순효과가 Allow 룰 하나로 유지되는 것은 **매치 조건이 좁기 때문**이다(§3의 전후 비교). 여기에 조건 없는 `Allow` 룰을 추가하면 해당 pod의 모든 egress가 order 200에서 종결되어 **차단 GNP 자체를 우회**하고, `Pass` 룰을 추가하면 남은 정책(차단 GNP)을 건너뛰고 profile(기본 허용)로 직행해 역시 가드레일이 깨진다. 허용 NP는 열 대상만 좁게 매칭하고, 나머지 트래픽 처리는 차단 GNP rule 3에 맡기는 형태만 안전하다. ### 2.4 order 체계 규약 | order 대역 | 용도 | 비고 | |---|---|---| | `100 – 299` | **허용 NP** 앱·네임스페이스별 | 예시는 200. 팀별로 대역 안에서 자유 배정 | | `500` | **차단 GNP** 가드레일 | 단일 객체 고정. 300–699는 플랫폼 예약 대역으로 비워둠 | | `1000` | **k8s NP** 자동 변환(`knp.default.*`) | 고정값. 이 설계에서는 DB에 관해 무력 | | 미지정 | — | 최후 평가로 밀림. 모든 Calico 정책에 order를 명시하는 것을 규약으로 | **간섭 규칙 — 대상 네임스페이스의 egress 통제는 Calico NP로 일원화한다.** rule 3의 Allow(500)는 종결이므로, 대상 ns에서 누군가 k8s NP(1000)로 egress를 제한하려 해도 DB 외 트래픽은 500에서 이미 허용되어 그 정책에 닿지 않는다. 실제로 Calico Allow 정책이 기존 k8s NP를 무력화해 혼란을 겪는 사례가 커뮤니티 이슈로 반복 보고된다(§8). **"통제 대상 ns의 egress 정책은 order 100–299의 Calico NP로만 작성"** 을 팀 규약으로 명문화할 것. ## 3. 트래픽 판정 흐름 pod에서 나가는 패킷 하나가 §2의 리소스들을 어떻게 통과하는지 한 장으로 정리한다. §5.4의 연결 테스트 매트릭스는 이 그림의 경로 5개를 그대로 검증하는 표다. ``` pod egress packet (veth 진입) | v Q1. ns label db-egress-control=enabled ? --no--> 통제 밖 (기본 허용) | yes = 롤아웃 전 상태 v Q2. order<500 허용 NP의 Allow 룰에 매칭 ? --yes-> ALLOW (종결) | no (pod selector + db-set + port) 연결 성립 SYN/ACK v Q3. dst IP in kind:db GNS ipset ? --yes-> Log -> DENY (DROP) | no 무응답 -> SYN 재전송 v -> connect timeout GNP rule 3 Allow (종결) DB 외 트래픽 — DNS·사내 API·메시는 영향 없음 ``` **그림 4** — Q1은 namespaceSelector, Q2는 허용 NP(200), Q3은 차단 GNP(500)의 rule 1·2, 마지막 상자는 rule 3에 해당한다. 각 판정이 §2의 어느 줄에서 오는지 1:1로 대응된다. 이 그림에 **implicit deny 경로가 없는 것이 이 설계의 안전 특성**이다 — rule 3이 항상 평가를 닫아주기 때문. 단, ns 라벨 없이 허용 NP만 존재하는 비정상 상태에서는 Q2 "아니오" 이후 갈 곳이 없어 implicit deny가 살아난다(§7-1). **허용 NP의 순효과 — "다른 대역 무영향"의 보장.** 허용 NP는 차단 정책을 수정하거나 구멍을 뚫는 것이 아니라, 먼저 평가되는 위치(order 200)에 좁은 Allow를 놓는 것이다. 라벨 붙은 ns의 pod 기준으로 NP 추가 전후의 판정 경로를 비교하면 순효과가 정확히 드러난다. | 트래픽 | 허용 NP 추가 전 | 허용 NP 추가 후 | |---|---|---| | `mysql-pay:3306` — 열려는 대상 | GNP rule 2 → **Deny** | NP Allow → **성공** — 유일한 변화 | | 다른 DB 세트·같은 IP 다른 포트 | GNP rule 2 → Deny | NP 무매칭 → GNP rule 2 → Deny **(동일)** | | 비DB 전부 — DNS·사내 API·메시 | GNP rule 3 → Allow | NP 무매칭 → GNP rule 3 → Allow **(동일)** | 2·3행이 동일한 것은 NP가 무해해서가 아니다. 어떤 정책이든 pod를 select하는 순간 그 방향의 기본값은 차단으로 뒤집히며(§1.2), 그 뒤집힌 기본값을 **차단 GNP rule 3이 다시 받아주기 때문에** 결과가 같아지는 것이다. 따라서 이 보장은 **해당 ns가 차단 GNP 적용 대상일 때만 성립**한다 — 라벨 없는 ns에 허용 NP만 있으면 §7-1의 금지 상태가 되고, §5.4 매트릭스 #6이 이를 그대로 재현한다. ## 4. 운영 적용 절차 순서가 곧 안전장치다. 특히 **4단계의 적용 순서(라벨과 허용 NP)** 는 §7-1 함정과 직결된다. | 단계 | 작업 | 내용 | |---|---|---| | 1 | 환경 확인 | Calico 버전 / iptables 백엔드 / natOutgoing 여부 확인(아래 커맨드) | | 2 | **GNS** 배포 | DBA에게 세트별 failover IP 전체 목록을 받아 등록. 아직 아무 정책도 참조하지 않으므로 무해 | | 3 | 섀도 관측 | v3.30+: 동일 spec의 `StagedGlobalNetworkPolicy`로 "차단됐을" 플로우를 flow log·Whisker에서 관측(§6.3). 구버전: rule 2(Deny)를 뺀 Log-only GNP를 라벨 ns에 적용해 커널 로그로 현행 DB 플로우 수집 | | 4 | 파일럿 ns 전환 | 관측으로 확정한 허용 목록을 NP로 작성. **허용 NP와 ns 라벨을 같은 PR·같은 sync로** 적용하되 간격을 최소화. 기존 커넥션 풀은 conntrack으로 유지되므로(§7-3) 순단 영향은 신규 연결에 한정 | | 5 | 검증 | §5.4 연결 테스트 매트릭스 전 항목 + §6.1 차단 로그 확인 | | 6 | 순차 확대 | ns 단위로 3→4→5 반복. GitOps 소유권: GNS·GNP = 플랫폼 리포(변경 시 DBA 리뷰), 허용 NP = 앱 리포(생성 시 플랫폼 리뷰) | | 7 | 안정화 | Log 룰 유지/축소 판단(§6.1 볼륨). 전면 default-deny 확장은 별도 과제로 분리 | ```bash # 1단계 — 환경 확인 kubectl get clusterinformation default \ -o jsonpath='{.spec.calicoVersion}' # Calico 버전 (3.30+ 여부) iptables --version # (nf_tables) 표기 시 iptables-nft 백엔드 calicoctl get ippool -o yaml | grep -E 'cidr|natOutgoing' # cidr: 10.244.0.0/16 # natOutgoing: true ← true면 방화벽·DB가 보는 소스는 노드 IP (§7-8) ``` > [!warning] 금지 상태 — 라벨 없는 ns에 허용 NP만 존재 > 허용 NP가 pod를 select하는 순간 그 pod의 egress 기본값은 차단으로 뒤집히는데, 라벨이 없으면 차단 GNP(와 그 안의 rule 3 Allow)가 붙지 않아 **DB 외 모든 egress가 implicit deny로 죽는다**(그림 4 주석). "NP만 먼저 넣고 라벨은 나중에"가 가장 위험한 순서다. 반대로 라벨을 먼저 붙이면 그 사이 신규 DB 연결만 잠시 차단될 뿐 DNS 등은 안전하다 — **순서를 골라야 한다면 항상 라벨이 먼저다.** ## 5. 검증·디버깅 — 트래픽 흐름 실측 ### 5.1 테스트 환경 구성 — `calico-egress-policy-test`와 라벨 설정 §5의 실측은 전용 네임스페이스 `calico-egress-policy-test`에서 수행한다. 첨부한 [`db-access-control-manifests.yaml`](files/db-access-control-manifests.yaml) 하나가 GNS·GNP에 이어 네임스페이스(스위치 라벨 포함)·테스트 클라이언트 2종·허용 NP까지 배포하고, §5.4 매트릭스의 전 행이 이 구성만으로 실행된다. > [!note] 적용 순서는 파일이 보장한다 > 매니페스트의 문서 순서(GNS → GNP → 라벨 붙은 Namespace → 클라이언트 → 허용 NP)가 곧 안전한 적용 순서다 — "라벨 먼저" 규약(§4, §7-1)이 **파일 구조로 강제**된다. 네임스페이스는 라벨이 붙은 "완성 상태"로 배포된다. ```yaml # db-access-control-manifests.yaml 중 테스트 구성부 (d)~(g) — 요약 apiVersion: v1 kind: Namespace metadata: name: calico-egress-policy-test labels: db-egress-control: "enabled" # 통제 스위치 — 차단 GNP의 namespaceSelector가 읽는다 --- apiVersion: apps/v1 kind: Deployment metadata: name: db-client-allowed namespace: calico-egress-policy-test spec: replicas: 1 selector: matchLabels: { app: db-client-allowed } template: metadata: labels: app: db-client-allowed # ← 허용 NP의 spec.selector가 읽는 pod 라벨 spec: containers: - name: client image: nicolaka/netshoot:v0.14 # 에어갭: 사내 레지스트리 미러 경로로 치환 command: ["sleep", "infinity"] resources: requests: { cpu: 10m, memory: 32Mi } limits: { cpu: 100m, memory: 64Mi } --- # db-client-denied — 위와 동일, app 라벨만 db-client-denied (허용에 매칭되지 않는 대조군) --- apiVersion: projectcalico.org/v3 kind: NetworkPolicy metadata: name: allow-db-mysql-pay namespace: calico-egress-policy-test spec: order: 200 selector: app == "db-client-allowed" types: [Egress] egress: - action: Allow protocol: TCP destination: namespaceSelector: global() selector: db-set == "mysql-pay" ports: [3306] ``` **클라이언트 설계.** 두 Deployment는 **pod 라벨 하나(`app`)만 다르다** — `db-client-allowed`는 허용 NP의 `spec.selector`에 매칭되고, `db-client-denied`는 어떤 허용에도 매칭되지 않는 대조군이다. 이미지는 `nc`·`tcpdump`·`nslookup`을 갖춘 netshoot을 쓰되 에어갭 환경이므로 사내 레지스트리 미러로 치환한다. 테스트 ns에는 `istio-injection` 라벨을 붙이지 않는다 — sidecar 없는 순수 경로로 검증하며, **운영에서도 DB 트래픽은 sidecar를 우회하므로 조건이 같다.** resources 블록은 요청·제한을 강제하는 어드미션 정책이 있는 클러스터를 대비한 것이다. **라벨 지도.** 이 설계에 사람이 다루는 라벨은 네 개가 전부이고, 각각 붙는 곳과 읽는 곳이 다르다. | 라벨 | 붙는 곳 | 읽는 곳 | 의미 | |---|---|---|---| | `db-egress-control=enabled` | Namespace | 차단 GNP `namespaceSelector` | 이 ns를 통제 대상에 편입 — 롤아웃·테스트 스위치 | | `app=db-client-allowed` | Pod (Deployment template) | 허용 NP `spec.selector` | 허용 범위 지정. 운영에서는 앱 라벨(`app=pay-api` 등)이 이 자리에 | | `kind=db` | GNS 전부 | 차단 GNP `destination.selector` | 차단의 그물 — 존·세트 전체 매칭 | | `db-set=mysql-pay` | 세트 GNS만 | 허용 NP `destination.selector` | 허용의 단위 — failover 집합 하나 | *이 외에 Calico가 네임스페이스 소속 리소스에 자동 부여하는 `projectcalico.org/namespace` 라벨이 §2.2의 가드에 쓰인다 — 사람이 붙이는 라벨이 아니다.* ```bash # 배포와 확인 kubectl apply -f db-access-control-manifests.yaml kubectl -n calico-egress-policy-test get pod --show-labels # NAME READY STATUS LABELS # db-client-allowed-6f8b7c9d4-x2k1p 1/1 Running app=db-client-allowed,... # db-client-denied-5d7f8b6c3-m9q4w 1/1 Running app=db-client-denied,... # 스위치 상태 확인 — 통제 대상 ns 전수 조회 (감사 시에도 이 한 줄) kubectl get namespace -l db-egress-control=enabled # 스위치 OFF — 값 변경이 아니라 키 제거: 키 이름 뒤에 '-' kubectl label namespace calico-egress-policy-test db-egress-control- # 스위치 ON (원복) kubectl label namespace calico-egress-policy-test db-egress-control=enabled # 정리 — 테스트 ns만 삭제 (클라이언트·허용 NP 함께 제거, GNS·GNP는 남는다) kubectl delete namespace calico-egress-policy-test # ⚠ kubectl delete -f 매니페스트 전체는 금지 — GNS·GNP까지 지워져 fail-open(§7-5) ``` 운영 네임스페이스의 라벨은 `kubectl label`이 아니라 **ns 매니페스트(ArgoCD 관리)에 선언**해 두는 것이 기본이다. 위의 명령형 조작은 테스트·비상 대응용이다. ### 5.2 정적 확인 — 선언한 것이 실체화됐는가 ```bash # ① 리소스 존재·순서 확인 calicoctl get globalnetworkset -o wide # NAME NETS # dbset-mysql-pay 10.20.30.11/32,10.20.30.12/32 # dbzone-all 10.20.0.0/16 calicoctl get globalnetworkpolicy -o wide # NAME ORDER SELECTOR # deny-db-egress 500 all() calicoctl get networkpolicy -n calico-egress-policy-test -o wide # NAME ORDER SELECTOR # allow-db-mysql-pay 200 app == "db-client-allowed" # knp.default.some-k8s-np 1000 ... ← k8s NP가 있다면 이렇게 변환·표시된다 # ② 대상 pod의 WEP과 veth 인터페이스 (해당 pod가 스케줄된 노드 확인용) calicoctl get workloadendpoint -n calico-egress-policy-test -o wide # NAME WORKLOAD NETWORKS INTERFACE # calico--policy--test.db-... db-client-allowed-6f... 10.244.3.17/32 cali1a2b3c4d5e6 # ③ GNS가 노드 커널의 ipset으로 실체화됐는지 — "정책이 안 먹는" 1차 원인 지점 sudo ipset list | grep -B 20 "10.20.30.11" # Name: cali40s:Fs4kQ7... ← 해시 이름의 세트 안에 # Members: # 10.20.30.11 # 10.20.30.12 ``` **③이 이 계층의 진실이다.** GNS YAML은 맞는데 통제가 동작하지 않으면 대부분 IP 오타·CIDR 표기 오류로 ipset에 원하는 IP가 없는 경우다. **선언(calicoctl)과 실체(ipset)를 항상 짝으로** 확인한다. ### 5.3 동적 추적 — 패킷이 실제로 지나가는 길 ``` pod veth iptables walk 10.244.3.17 -> cali1a2b3c4d5e6 -> FORWARD | +-> cali-FORWARD | +-> cali-from-wl-dispatch | +-> cali-fw-cali1a2b... | (this pod, egress) +-> cali-po-_a1b2... order 200 | allow NP -> ACCEPT (종결) | +-> cali-po-_c3d4... order 500 match-set cali40s:Fs4kQ7 dst -> LOG + DROP / else ACCEPT ``` **그림 5** — Felix가 컴파일한 실체. 정책은 pod별 `cali-fw-*` 체인에서 order 순으로 정책별 체인(`cali-po-*`)을 호출하는 iptables 구조가 되고, GNS는 `match-set`이 참조하는 ipset이 된다. 해시 이름(`_c3d4…`, `Fs4kQ7…`)은 환경마다 다르며 **룰의 comment로 원본 정책을 식별**한다. | 관측 대상 | 커맨드 | |---|---| | veth 패킷 | `tcpdump -ni cali1a2b3c4d5e6 'tcp port 3306'` | | 체인 구조 | `iptables-save -t filter \| grep cali1a2b3c4d5e6` | | 판정 실시간 | `watch -d -n1 "iptables -L cali-po-_c3d4e5f6g7h8 -n -v"` (pkts 증가 = 그 룰이 판정) | | ipset 내용 | `ipset list cali40s:Fs4kQ7...` (목적지 IP 포함 여부) | ```bash # 미허용 클라이언트에서 시도하며, 그 pod가 뜬 노드에서 관찰한다 kubectl -n calico-egress-policy-test exec deploy/db-client-denied -- nc -zv -w 3 10.20.30.11 3306 & # ① 판정 룰 카운터 — Deny 룰의 pkts가 시도 순간 증가 watch -d -n1 "sudo iptables -L cali-po-_c3d4e5f6g7h8 -n -v" # pkts bytes target prot opt in out source destination # 3 180 LOG all -- * * 0.0.0.0/0 0.0.0.0/0 match-set cali40s:Fs4kQ7 dst ... # 3 180 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 match-set cali40s:Fs4kQ7 dst ... # ② 패킷 레벨 — DROP은 응답이 없다. SYN 재전송만 반복되는 모양이 "차단의 정상 형태" sudo tcpdump -ni cali1a2b3c4d5e6 'tcp port 3306' # 10:02:11.10 IP 10.244.3.17.51234 > 10.20.30.11.3306: Flags [S], ... # 10:02:12.11 IP 10.244.3.17.51234 > 10.20.30.11.3306: Flags [S], ... ← 재전송 # 10:02:14.15 IP 10.244.3.17.51234 > 10.20.30.11.3306: Flags [S], ... ← 재전송 # (SYN/ACK 없음 → 앱은 connect timeout) # ③ 기존 연결(커넥션 풀) 확인 — 정책 적용 후에도 ESTABLISHED는 살아 있다 sudo conntrack -L -p tcp --dport 3306 -d 10.20.30.11 2>/dev/null # tcp 6 431990 ESTABLISHED src=10.244.3.17 dst=10.20.30.11 sport=48822 dport=3306 ... # 필요 시 의도적 절단(신규 판정 강제) — 대상·시점을 합의하고 실행할 것 sudo conntrack -D -p tcp --dport 3306 -d 10.20.30.11 ``` iptables 백엔드가 nftables(`iptables --version`에 `nf_tables` 표기)면 위 명령들은 `iptables-nft`/`iptables-nft-save` 계열로 읽어야 한다. kubespray 노드 OS 구성에 따라 다르다. ### 5.4 연결 테스트 매트릭스 그림 4의 경로 5개를 #1–#5로 검증하고, #6은 §7-1의 금지 상태를 의도적으로 재현하는 선택 실험이다. 파일럿 전환(§4-5단계)마다 #1–#5를 수행한다. 모든 소스는 §5.1에서 배포한 클라이언트다. | # | 소스 | 대상 | 기대 | 관측 방법·판정 근거 | |---|---|---|---|---| | 1 | 허용 클라이언트 `db-client-allowed` | `mysql-pay:3306` | **성공** | `nc -zv` succeeded / tcpdump에 SYN·SYN/ACK 왕복 / 허용 NP 체인 카운터 증가 | | 2 | 미허용 클라이언트 `db-client-denied` | `mysql-pay:3306` | **차단** | `nc` timeout / Deny 카운터 증가 / §6.1 커널 로그 | | 3 | `db-client-allowed` | 같은 IP 다른 포트 `:3307` · 존 내 다른 IP | **차단** | 허용은 "세트 + 포트" 단위임을 증명 — timeout | | 4 | 스위치 OFF 후 `db-client-denied` | `mysql-pay:3306` | **성공(통제 밖)** | 라벨 제거 = 롤아웃 전 기준선. 라벨 원복 시 #2로 회귀해야 스위치 정상 | | 5 | 대상 ns의 아무 클라이언트 | 비DB 목적지(DNS·사내 API) | **성공** | rule 3 안전핀 검증 — 평소 트래픽에 회귀 없음 | | 6 | (선택) 스위치 OFF 상태의 `db-client-allowed` | DNS(`kubernetes.default`) | **실패** | §7-1 금지 상태 재현 — NP 단독 select의 implicit deny 체감. 확인 즉시 라벨 원복 | ```bash # 매트릭스 실행 스크립트 — 행 번호 순. 전제: §5.1 매니페스트 배포 완료 NS=calico-egress-policy-test kubectl -n $NS exec deploy/db-client-allowed -- nc -zv -w 3 10.20.30.11 3306 # 1 → succeeded! kubectl -n $NS exec deploy/db-client-denied -- nc -zv -w 3 10.20.30.11 3306 # 2 → timeout kubectl -n $NS exec deploy/db-client-allowed -- nc -zv -w 3 10.20.30.11 3307 # 3 → timeout (포트 단위) kubectl -n $NS exec deploy/db-client-allowed -- nc -zv -w 3 10.20.99.99 5432 # 3 → timeout (존 내 타 IP) kubectl -n $NS exec deploy/db-client-denied -- nslookup kubernetes.default # 5 → 응답 정상 (rule 3) kubectl label namespace $NS db-egress-control- # 스위치 OFF kubectl -n $NS exec deploy/db-client-denied -- nc -zv -w 3 10.20.30.11 3306 # 4 → succeeded (통제 밖) kubectl -n $NS exec deploy/db-client-allowed -- nslookup kubernetes.default # 6 → timeout (§7-1 재현) kubectl label namespace $NS db-egress-control=enabled # 원복 — 반드시 실행 ``` > [!note] ping은 검증 수단이 아니다 > 차단 룰은 프로토콜 무제한이라 ICMP도 막고, 허용 NP는 TCP/3306만 열므로 **허용 pod에서도 DB로의 ping은 실패**한다. 검증은 반드시 `nc`·실제 클라이언트 등 대상 포트의 TCP로 한다. 차단의 관측 형태가 "거부(RST)"가 아니라 **"무응답(timeout)"** 인 것도 정상이다 — Deny는 DROP이다. ## 6. 로깅·모니터링·가시화 ### 6.1 차단 로그 — Log 액션과 커널 로그 §2.2 rule 1의 Log 액션은 Felix가 iptables LOG 타깃으로 컴파일한다. 기록 위치는 **소스 pod가 떠 있는 노드의 커널 로그**다 — 중앙이 아니라 노드별로 남는다는 점이 수집 설계의 출발점이다. ```bash # 해당 노드에서 실시간 확인 # (기본 접두어 calico-packet — FelixConfiguration.spec.logPrefix로 변경 가능) journalctl -k -f | grep calico-packet # kernel: calico-packet: IN=cali1a2b3c4d5e6 OUT=bond0 SRC=10.244.3.17 DST=10.20.30.11 # PROTO=TCP SPT=51234 DPT=3306 SYN ... ``` 이 라인 하나에 사고 조사에 필요한 것이 다 있다: 어느 pod가(`SRC` + `IN=`veth→WEP 역추적), 어느 DB로(`DST`·`DPT`), 언제 시도했는가. 노드 journald를 이미 수집하고 있는 사내 로그 파이프라인에서 `calico-packet` 접두어만 필터·인덱싱하면 중앙 검색과 알림(차단 발생 시 통보)이 바로 성립한다. > [!note] 볼륨 주의 > LOG는 **패킷 단위**라 차단 1회 시도에도 SYN 재전송만큼 여러 줄이 남는다. 배치 잡이 루프에서 재시도하는 오구성 상황에서는 초당 수백 줄까지 갈 수 있다. 도입·확대기에는 유지하되, 안정화 후에는 rule 1 제거(카운터·flow log로 대체) 또는 유지 여부를 명시적으로 결정한다(§4-7단계). ### 6.2 정량 지표 — 카운터 Deny 룰의 iptables 카운터(§5.3-①)는 "차단이 몇 번 일어났는가"의 가장 값싼 지표다. node_exporter textfile collector로 `cali-po-*` Deny 카운터를 주기 수집하면 "차단 발생 > 0" 알림을 Prometheus에 얹을 수 있다. Felix의 기본 Prometheus 메트릭은 데이터플레인 상태(체인 수·프로그래밍 지연) 중심이라 **정책별 차단 수는 주지 않는다** — 정책 단위의 정식 답은 6.3의 flow log다. ### 6.3 Calico 3.30+ — flow log(Goldmane)와 Whisker 콘솔 v3.30부터 상용판에 있던 관측 스택이 오픈소스에 포함됐다. **Goldmane**은 플로우 로그와 정책 판정(verdict)을 gRPC API로 내주는 수집기이고, **Whisker**는 그 위의 웹 콘솔로 소스·목적지·**판정 결과(Allow/Deny)** 로 플로우를 필터링해 보여준다. GNS로 등록한 대역은 플로우에 **세트 이름이 메타데이터로 붙어** "어느 pod가 dbset-mysql-pay에 접근했는가"를 IP 해석 없이 바로 읽을 수 있다 — 이 설계의 GNS가 **통제 단위이자 관측 단위**가 되는 지점이다. ```bash # Whisker 콘솔 열기 (신규 3.30+ 설치는 기본 포함, 업그레이드 클러스터는 별도 활성화 필요) kubectl -n calico-system port-forward svc/whisker 8081:8081 # → 브라우저 localhost:8081 — dest_name=dbset-mysql-pay, action=deny 필터로 차단 플로우 확인 ``` 같은 릴리스의 **StagedGlobalNetworkPolicy / StagedNetworkPolicy**는 §4-3단계의 섀도 관측 도구다. 동일 spec을 staged로 두면 실제 차단 없이 "시행됐다면 이 판정"이 flow log에 남아, enforce 전에 허용 목록의 누락을 확인할 수 있다. 에어갭 환경이므로 3.30 업그레이드·활성화 시 Goldmane·Whisker 이미지의 사내 레지스트리 미러가 선행돼야 한다. ### 6.4 Kiali가 이 트래픽을 못 보는 이유 Kiali의 그래프는 Envoy(sidecar) 메트릭에서 나온다. 이 환경의 DB 트래픽은 `excludeOutboundPorts`로 sidecar를 우회하므로 **Kiali에는 처음부터 존재하지 않는 트래픽**이다. "Kiali에 안 보인다 = 통제가 안 된다"가 아니며, 이 통제의 관측 책임 계층은 Calico(6.1–6.3)다. **메시 안 트래픽은 Kiali, DB 직결 트래픽은 Calico** — 관측 도구의 관할 경계를 팀에 명시해 둘 것. ## 7. 운영 함정 8가지 | # | 함정 | 증상 한 줄 | |---|---|---| | 1 | 라벨 없는 ns에 허용 NP 단독 존재 | 해당 pod의 DNS 포함 모든 egress가 죽는다 | | 2 | GNS에 failover IP 누락 | DB failover 순간 정상 앱이 일제히 timeout | | 3 | conntrack의 기존 연결 잔존 | 차단 후에도 이전 연결은 계속 통신 — 검증 착시·회수 지연 | | 4 | k8s NetworkPolicy 병용 간섭 | 대상 ns의 k8s NP egress 규칙이 조용히 무력화 | | 5 | GNS 삭제·prune = fail-open | 차단 매칭 대상이 사라져 통제 전체가 무효 | | 6 | Log 룰 볼륨 폭주 | 오구성 재시도 루프가 노드 저널을 채운다 | | 7 | 허용 NP 생성 권한 미통제 | 정책 생성 권한 = DB 접근 승인 권한이 된다 | | 8 | natOutgoing 전제 불일치 | 방화벽 계층의 허용 규칙과 소스 식별이 어긋난다 | **1 — NP 단독 존재.** 원인은 기본값 반전(§1.2): NP가 pod를 select하는 순간 egress 기본이 차단이 되는데, 라벨이 없으면 rule 3 Allow를 가진 GNP가 붙지 않는다. 대응: "라벨 먼저" 순서 규약(§4), 그리고 리뷰 체크리스트에 "이 NP의 ns에 `db-egress-control=enabled`가 있는가"를 포함. **2 — failover IP 누락.** TTL 1초 환경에서 failover는 언제든 일어난다. GNS는 "현재 IP"가 아니라 **"가능한 IP 집합"**으로 정의하고(§2.1), DB IP 변경 절차에 GNS PR을 필수 단계로 묶는다. 검증: §5.2-③으로 failover 대상 IP가 ipset에 실재하는지 확인. **3 — conntrack 잔존.** 정책은 새 판정에만 관여하고 ESTABLISHED 흐름은 conntrack이 통과시킨다. "차단했는데 아직 통신 중"은 버그가 아니라 **이 계층의 정의**다. 접근 회수를 즉시 완성해야 할 때만 §5.3-③의 `conntrack -D`로 절단한다 — 실행 전 대상 flow와 시점을 반드시 합의할 것. **4 — k8s NP 간섭.** rule 3 Allow(500)가 종결이라 order 1000의 k8s NP에는 트래픽이 닿지 않는다. 반대로 k8s NP로는 500의 Deny를 앞설 수도 없다(§2.4). 대응: 통제 대상 ns의 egress 정책은 order 100–299의 Calico NP로 일원화하고, 이를 온보딩 문서에 명문화. **5 — GNS 삭제 = fail-open.** 차단 룰은 `kind == "db"` 셀렉터로 GNS를 참조하므로 GNS가 지워지면 **매칭 대상이 소멸해 조용히 전면 허용**이 된다. ArgoCD prune 대상에서 보호(annotation)하고, GNS 리소스의 삭제 권한을 플랫폼 계정으로 제한한다. 감시: dbzone-all ipset 부재 알림. **6 — Log 볼륨.** §6.1 콜아웃 참조. 도입기 유지 → 안정화 후 명시적 결정. **7 — 정책 생성 권한.** 이 설계에서 DB 문을 여는 유일한 수단이 order<500 Calico NP이므로(§2.3), **NP 생성이 곧 승인**이다. RBAC로 `networkpolicies.crd.projectcalico.org` 생성 주체를 좁히거나, Kyverno로 "order<500 정책은 플랫폼 승인 라벨 필수 / db-set 값은 화이트리스트" 같은 어드미션 규칙을 얹는다. **8 — natOutgoing 전제.** Calico 계층은 SNAT 전이라 pod 단위를 보고, 방화벽·DB는 SNAT 후 노드 IP를 본다(§1.1). 방화벽 규칙이 "노드 대역→DB"로 넓게 열려 있는 전제를 확인하고, 워크로드 식별은 이 문서의 Calico 계층이 담당한다는 역할 분담을 명시한다. IPPool을 no-NAT로 바꾸는 변경이 생기면 방화벽 규칙도 함께 재설계 대상이다. ## 8. 참고 문서 - 외부 대역 통제 패턴(GNS+정책): [docs.tigera.io — Use external IPs or networks rules in policy](https://docs.tigera.io/calico/latest/network-policy/policy-rules/external-ips-policy) - 리소스 레퍼런스: [GlobalNetworkSet](https://docs.tigera.io/calico/latest/reference/resources/globalnetworkset) · [GlobalNetworkPolicy](https://docs.tigera.io/calico/latest/reference/resources/globalnetworkpolicy) · [NetworkPolicy](https://docs.tigera.io/calico/latest/reference/resources/networkpolicy) - 기본값·k8s 호환 규칙: [Enable a default deny policy](https://docs.tigera.io/calico/latest/network-policy/get-started/kubernetes-default-deny) - 관측(3.30+): [Enable flow logs API & Whisker](https://docs.tigera.io/calico/latest/observability/enable-whisker) · [View flow logs in Whisker](https://docs.tigera.io/calico/latest/observability/view-flow-logs) - k8s NP 간섭 실사례: [projectcalico/calico#10168](https://github.com/projectcalico/calico/issues/10168) ## 관련 문서 - [DB 연결은 egress 경유 제외 — excludeOutboundPorts 타당성 검토](/docs/istio/egress/db-egress-exclusion/) ([원문 HTML](/docs/istio/egress/db-egress-exclusion/files/db-egress-exclusion-proposal.html)) — **이 문서의 선행 결정문.** 거기서 말하는 통제 3층(① 존 방화벽 · ② Calico egress NetworkPolicy · ③ DB 계정·TLS·감사) 중 **②층의 구현 가이드**가 이 문서다 - [Egress 신원 통제 — TLS Passthrough + Calico 대안](/docs/istio/egress/identity-without-mtls/) — 왜 pod-selector가 그 자체로 "비-핸드셰이크 신원"인지. 이 문서 §1.1(SNAT 이전이라 소스가 pod 단위로 보인다)의 보안 논거 쪽 정본 - [Outbound 집약 설계서 — egress gateway로 Squid 경로 일원화](/docs/istio/egress/squid-consolidation-guide/) ([원문 HTML](/docs/istio/egress/squid-consolidation-guide/files/egress-gateway-squid-consolidation-guide.html)) — 외부 HTTPS 경로의 설계 본체. §7.1의 "Calico 이중 잠금"(mesh 밖 경로 차단)이 이 문서와 같은 계층 - [Egress 4-CRD 멘탈모델](/docs/istio/egress/crd-mental-model/) — mesh 경유 트래픽 쪽의 대응 개념. Calico(커널 IP 기반)와 Istio(프록시 설정 기반)의 통제 축이 어떻게 다른지 대조용