homelab89 Docs Logs Legacy Files ☰ TOC 🌓
guidek8s 2026-08-18caliconetworkpolicyglobalnetworksetglobalnetworkpolicyegressdatabaseiptablesipsetkubernetes

Calico NetworkPolicy 기반 DB 접근 통제 — 구성·검증·운영 가이드

원본은 스타일링된 HTML 가이드 + 배포용 매니페스트

이 글의 원문은 SVG 다이어그램 5종(트래픽 경로·평가 모델·참조 관계·판정 흐름·iptables 체인 워크)을 포함한 HTML 가이드다. 매니페스트는 GNS 2 + GNP 1 + 네임스페이스 + 클라이언트 2종 + 허용 NP를 적용 순서대로 담은 단일 파일이다.

이 페이지는 같은 내용의 markdown 정리본(SVG는 ASCII로 대체). 선행 문서: DB 연결은 egress 경유 제외 — 그 결정문이 말하는 통제 3층 중 ②층(Calico egress NetworkPolicy)의 구현 가이드가 이 문서다.

이 문서 전체를 관통하는 한 줄

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 트래픽은 excludeOutboundPortssidecar를 거치지 않고 직결됨.
  • 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:<hash> 형태의 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 한 파일에 여기의 리소스 전부와 테스트 네임스페이스·클라이언트 2종까지 적용 순서대로 들어 있어 그대로 배포하면 §5의 검증까지 이어진다. 운영 적용 시에는 GNS·GNP를 플랫폼 리포로, Namespace·허용 NP를 앱 리포로 분리한다(§4-6단계).

2.1 GNS — DB 대역 정의

# (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 대역 차단

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 예제가 쓰는 표준 가드다.

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·&&·||)를 받는다. “지정한 네임스페이스 또는 라벨 붙은 네임스페이스"라는 요구는 아래 ②혼합형이 그대로 표현한다.

# ① 이름 지정 — 모든 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 — 네임스페이스/파드 단위 허용

# 변형 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 거버넌스).

허용 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 확장은 별도 과제로 분리
# 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)
금지 상태 — 라벨 없는 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 하나가 GNS·GNP에 이어 네임스페이스(스위치 라벨 포함)·테스트 클라이언트 2종·허용 NP까지 배포하고, §5.4 매트릭스의 전 행이 이 구성만으로 실행된다.

적용 순서는 파일이 보장한다

매니페스트의 문서 순서(GNS → GNP → 라벨 붙은 Namespace → 클라이언트 → 허용 NP)가 곧 안전한 적용 순서다 — “라벨 먼저” 규약(§4, §7-1)이 파일 구조로 강제된다. 네임스페이스는 라벨이 붙은 “완성 상태"로 배포된다.

# 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의 가드에 쓰인다 — 사람이 붙이는 라벨이 아니다.

# 배포와 확인
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 정적 확인 — 선언한 것이 실체화됐는가

# ① 리소스 존재·순서 확인
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 포함 여부)
# 미허용 클라이언트에서 시도하며, 그 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 --versionnf_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 체감. 확인 즉시 라벨 원복
# 매트릭스 실행 스크립트 — 행 번호 순. 전제: §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                          # 원복 — 반드시 실행
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가 떠 있는 노드의 커널 로그다 — 중앙이 아니라 노드별로 남는다는 점이 수집 설계의 출발점이다.

# 해당 노드에서 실시간 확인
# (기본 접두어 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 접두어만 필터·인덱싱하면 중앙 검색과 알림(차단 발생 시 통보)이 바로 성립한다.

볼륨 주의

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가 통제 단위이자 관측 단위가 되는 지점이다.

# 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. 참고 문서

관련 문서

Files