Platform Engineering · Network Security · Calico (iptables dataplane)

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

클러스터 외부 DB 대역에 대한 egress 통제를 Calico 공식 문서의 기본 패턴 (GlobalNetworkSetGlobalNetworkPolicy 차단NetworkPolicy 허용)으로 구성하고, 운영 환경에 적용 가능한 수준의 검증·디버깅·로깅·가시화 방법까지 다룬다.

이 문서 전체를 관통하는 한 줄
DB 대역으로 나가는 트래픽은 order 500의 가드레일에서 전부 막고, 그보다 먼저 평가되는 order 200의 허용 정책만이 문을 연다.
order 0 평가 방향 → 허용 NetworkPolicy · 200 차단 GlobalNetworkPolicy · 500 k8s NP · 1000
목차
  1. 개요 — 전제·범위·용어
  2. 배경 개념 3가지
  3. 리소스 구성과 YAML
  4. 트래픽 판정 흐름
  5. 운영 적용 절차
  6. 검증·디버깅 — 트래픽 흐름 실측
  7. 로깅·모니터링·가시화
  8. 운영 함정 8가지
  9. 참고 문서

0개요 — 전제·범위·용어

0.1 환경 전제

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) NetworkPolicyprojectcalico.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 / natOutgoingpod가 클러스터 밖으로 나갈 때 소스 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 (ns: calico-egress-policy-test) app istio-proxy (운영 pod) DB 포트는 excludeOutboundPorts로 sidecar 우회 → app이 직접 연결 veth (caliXXXX) ★ 정책 적용 지점 egress: cali-fw-* 체인 ingress: cali-tw-* 체인 Worker Node natOutgoing=true면 SNAT: src → node IP Zone Firewall node 대역 → DB VIP:port 허용 DB Zone VIP + failover IP들 계정·pg_hba·TLS DNS TTL=1s Calico 계층(★)에서는 아직 SNAT 전이라 소스가 pod 단위로 보인다 — 워크로드 단위 통제가 가능한 이유
그림 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를 라벨보다 먼저 만들면 장애)의 원인이다.

order 낮음 = 먼저 평가 order 높음 = 나중 허용 NP · order 200 Allow → db-set:mysql-pay tcp/3306 차단 GNP · order 500 rule1 Log → kind:db rule2 Deny → kind:db rule3 Allow (그 외 전부) k8s NP · 1000 knp.default.* 자동 변환 order 미지정 = 최후 평가 종결 규칙 첫 매칭 Allow/Deny에서 평가 종료. Log는 비종결 — 기록 후 다음 룰 계속. 기본값 규칙 select하는 정책 0개 → 기본 허용. 1개 이상 select + 끝까지 무매칭 → implicit 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 dbzone-all labels: kind=db nets: 10.20.0.0/16 DB 존 전체 — 차단의 그물 GNS dbset-mysql-pay labels: kind=db, db-set=mysql-pay nets: 10.20.30.11/32, 10.20.30.12/32 failover 세트 전체 — 허용의 단위 GNP deny-db-egress · order 500 namespaceSelector: db-egress-control == "enabled" destination.selector: kind == "db" 라벨 붙은 네임스페이스의 모든 pod에 적용 NP allow-db-mysql-pay · calico-egress-policy-test · 200 selector: app == "db-client-allowed" destination.selector: db-set == "mysql-pay" ports: [3306] 해당 pod에게만, 해당 세트·포트만 연다 kind == "db" 라벨로 둘 다 매칭 db-set 라벨로 정확히 한 세트만 매칭
그림 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.1GNS GlobalNetworkSet — 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.kinddb차단 정책이 매칭하는 넓은 라벨. 존 GNS·세트 GNS 모두에 붙인다.
metadata.labels.db-setmysql-pay허용 정책이 매칭하는 좁은 라벨. 세트 GNS에만 붙인다.
spec.netsCIDR 목록이 집합이 대표하는 실제 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.2GNP GlobalNetworkPolicy — 지정 네임스페이스의 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 Logrule 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"}

변형을 쓸 때는 세 가지를 함께 규약화한다. ① 부정형 연산자(!has·not in·!=)는 라벨이 없는 네임스페이스도 매칭한다 — opt-out이 성립하는 근거이자, != "enabled" 같은 식을 무심코 썼을 때 전 클러스터를 쓸어 담는 이유다. ② opt-out에서는 db-egress-exempt 라벨 부여가 곧 통제 해제 승인이므로, ns 라벨 수정 권한을 RBAC·Kyverno로 묶는다(§7-7과 같은 논리). hostNetwork pod(kube-proxy·calico-node 등)는 WEP이 아니라 애초에 정책 대상 밖이며, 제외 목록이 실제로 보호하는 것은 coredns처럼 pod 네트워크를 쓰는 시스템 컴포넌트다. ③ 권장 경로: opt-in으로 편입을 끝낸 뒤 kubectl get ns -l '!db-egress-control'로 잔여(통제 밖) ns가 없음을 확인하고, staged 관측(§4-3단계)을 거쳐 셀렉터만 ③으로 교체한다. opt-out이 되면 모든 ns가 항상 GNP 아래에 있어 §7-1의 "허용 NP 단독 존재" 상태가 구조적으로 불가능해진다.

2.3NP NetworkPolicy — 네임스페이스/파드 단위 허용

# 변형 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" # (e) 허용 클라이언트에게만
  types:
    - Egress
  egress:
    - action: Allow
      protocol: TCP
      destination:
        namespaceSelector: global() # 전역 리소스(GNS)를 평가 대상으로
        selector: db-set == "mysql-pay"
        ports:
          - 3306
---
# 변형 B — 네임스페이스 전체 허용 (selector만 다름)
#   spec.selector: all()   ← calico-egress-policy-test ns의 모든 pod에게 mysql-pay:3306 개방
필드역할
order: 200500보다 앞이라 먼저 평가되고, Allow는 종결이므로 뒤의 차단에 닿지 않는다. 600으로 바꾸면 rule 2 Deny가 먼저 매칭되어 이 정책은 영원히 평가되지 않는다 — 허용이 "안 먹히는" 사고의 제1 원인.
spec.selector허용 범위. app == "db-client-allowed"(pod 단위, 매니페스트 (e) 클라이언트와 짝) 또는 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.4order 체계 규약

order 대역용도비고
100 – 299허용 NP 앱·네임스페이스별예시는 200. 팀별로 대역 안에서 자유 배정.
500차단 GNP 가드레일단일 객체 고정. 300–699는 플랫폼 예약 대역으로 비워둔다.
1000k8s 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 패킷 (veth 진입) Q1. 소속 ns에 db-egress-control=enabled 라벨? (차단 GNP의 namespaceSelector) 아니오 통제 밖 — 기본 허용 (롤아웃 전 상태) Q2. order<500 허용 NP의 Allow 룰에 매칭? (pod 셀렉터 + db-set 라벨 + 포트 모두 일치) ALLOW — 종결 연결 성립 (SYN/ACK 왕복) 아니오 Q3. 목적지 IP가 kind:db GNS 대역(ipset)에 포함? (차단 GNP rule 1·2의 destination) Log → DENY(DROP) 무응답 — 앱은 SYN 재전송 끝에 connect timeout 아니오 차단 GNP rule 3 Allow — 종결 DB 외 트래픽(DNS·사내 API·메시)은 영향 없음 이 그림에 implicit deny 경로가 없는 것이 이 설계의 안전 특성이다 — rule 3이 항상 평가를 닫아주기 때문. 단, ns 라벨 없이 허용 NP만 존재하는 비정상 상태에서는 Q2 "아니오" 이후 갈 곳이 없어 implicit deny가 살아난다(§7-1).
그림 4 — 판정 흐름. Q1은 namespaceSelector, Q2는 허용 NP(200), Q3은 차단 GNP(500)의 rule 1·2, 마지막 상자는 rule 3에 해당한다. 각 판정이 §2의 어느 줄에서 오는지 1:1로 대응된다.

허용 NP의 순효과 — "다른 대역 무영향"의 보장. 허용 NP는 차단 정책을 수정하거나 구멍을 뚫는 것이 아니라, 먼저 평가되는 위치(order 200)에 좁은 Allow를 놓는 것이다. 라벨 붙은 ns의 pod 기준으로 NP 추가 전후의 판정 경로를 비교하면 순효과가 정확히 드러난다.

트래픽허용 NP 추가 전허용 NP 추가 후
mysql-pay:3306 — 열려는 대상GNP rule 2 → DenyNP Allow → 성공 — 유일한 변화
다른 DB 세트·같은 IP 다른 포트GNP rule 2 → DenyNP 무매칭 → GNP rule 2 → Deny (동일)
비DB 전부 — DNS·사내 API·메시GNP rule 3 → AllowNP 무매칭 → 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 여부 확인(아래 커맨드).
2GNS 배포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=enabledNamespace차단 GNP namespaceSelector이 ns를 통제 대상에 편입 — 롤아웃·테스트 스위치
app=db-client-allowedPod (Deployment template)허용 NP spec.selector허용 범위 지정. 운영에서는 앱 라벨(app=pay-api 등)이 이 자리에 온다
kind=dbGNS 전부차단 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
NAME                 STATUS   AGE
calico-egress-policy-test   Active   2m

# 스위치 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 10.244.3.17 veth cali1a2b3c4d5e6 FORWARD → cali-FORWARD → cali-from-wl-dispatch cali-fw-cali1a2b… 이 pod의 egress 전용 체인 cali-po-_a1b2… (order 200) 허용 NP — 매칭 시 ACCEPT 종결 cali-po-_c3d4… (order 500) match-set cali40s:Fs4kQ7 dst → LOG · DROP / 그 외 ACCEPT 각 지점의 관측 커맨드 veth ······· tcpdump -ni cali1a2b3c4d5e6 'tcp port 3306' 체인 구조 ··· iptables-save -t filter | grep cali1a2b3c4d5e6 (정책명은 룰의 comment로 식별) 판정 실시간 · watch -d -n1 "iptables -L cali-po-_c3d4e5f6g7h8 -n -v" (pkts 카운터 증가 = 그 룰이 판정) ipset ······ ipset list cali40s:Fs4kQ7... (목적지 IP 포함 여부)
그림 5 — Felix가 컴파일한 실체. 정책은 pod별 cali-fw-* 체인에서 order 순으로 정책별 체인(cali-po-*)을 호출하는 iptables 구조가 되고, GNS는 match-set이 참조하는 ipset이 된다. 해시 이름(_c3d4…, Fs4kQ7…)은 환경마다 다르며 룰의 comment로 원본 정책을 식별한다.
# 미허용 클라이언트에서 시도하며, 그 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 커널 로그
3db-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.3Calico 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.4Kiali가 이 트래픽을 못 보는 이유

Kiali의 그래프는 Envoy(sidecar) 메트릭에서 나온다. 이 환경의 DB 트래픽은 excludeOutboundPorts로 sidecar를 우회하므로 Kiali에는 처음부터 존재하지 않는 트래픽이다. "Kiali에 안 보인다 = 통제가 안 된다"가 아니며, 이 통제의 관측 책임 계층은 Calico(6.1–6.3)다. 메시 안 트래픽은 Kiali, DB 직결 트래픽은 Calico — 관측 도구의 관할 경계를 팀에 명시해 둘 것.

7운영 함정 8가지

#함정증상 한 줄
1라벨 없는 ns에 허용 NP 단독 존재해당 pod의 DNS 포함 모든 egress가 죽는다
2GNS에 failover IP 누락DB failover 순간 정상 앱이 일제히 timeout
3conntrack의 기존 연결 잔존차단 후에도 이전 연결은 계속 통신 — 검증 착시·회수 지연
4k8s NetworkPolicy 병용 간섭대상 ns의 k8s NP egress 규칙이 조용히 무력화
5GNS 삭제·prune = fail-open차단 매칭 대상이 사라져 통제 전체가 무효
6Log 룰 볼륨 폭주오구성 재시도 루프가 노드 저널을 채운다
7허용 NP 생성 권한 미통제정책 생성 권한 = DB 접근 승인 권한이 된다
8natOutgoing 전제 불일치방화벽 계층의 허용 규칙과 소스 식별이 어긋난다

표는 색인. 아래에서 원인과 대응을 항목별로 해설한다.

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

작성 2026-08 · 전제: 온프레미스 Kubernetes(kubespray) · Calico iptables 데이터플레인 · Istio(DB 포트 sidecar 우회) · DB는 클러스터 외부, DNS TTL 1초 · 동봉: db-access-control-manifests.yaml — GNS·GNP·테스트 ns·클라이언트·허용 NP 완성 예시