# 60-airgap-lab — 폐쇄망에서 ingress/egress gateway 테스트 랩 폐쇄망(air-gapped) 클러스터에서 인터넷 없이 Istio ingress/egress gateway를 테스트하기 위한 자기완결형 랩. "외부 세계"를 클러스터 안에서 모사함 — 부품은 전부 50-dns-resolution 랩과 20-egress/dual-gateway에서 실측 검증된 패턴의 재조립임. 구성은 **파일을 하나씩 apply 하며 단계마다 확인**하는 방식이 기본임(아래 1절). 각 단계는 실행 → 확인 한 쌍이고, 확인이 실패하면 다음으로 넘어가지 않음. > 상태: **초안** — manifest는 dry-run + `istioctl analyze` 검증까지. 홈랩 트래픽 완주는 리뷰 후. ## 구조 ``` +----------------------------- air-gapped cluster ------------------------------+ | | | ns: lab-ext (no injection) <- "the internet" | | [fake-dns] CoreDNS hosts plugin, ttl 5s api/blocked/www.lab.external | | [ext-a] [ext-b] nginx TLS (private CA) <- fake external servers | | [ext-client] netshoot <- fake external USER | | | | ns: istio-system | | [istiod] [ingressgateway] [egressgateway] images from corp registry | | Gateway: lab-ingress / lab-egress <- gateway workload ns rule | | | | ns: lab-apps (injection=enabled) <- "our services" | | [web-a] [web-b] ingress backends | | [mesh-client] egress source, dnsConfig -> fake-dns | | [Sidecar: REGISTRY_ONLY] ns-scoped outbound policy | | | | INGRESS: ext-client --DNS(www)--> ingressgateway --TLS term--> web-a/web-b | | EGRESS : mesh-client --SNI route--> egressgateway --passthrough--> ext-a | | BLOCKED: mesh-client --> blocked.lab.external = DNS ok, no SE -> reset | +--------------------------------------------------------------------------------+ ``` ## 리소스 배치 요약 (namespace 일람) | ns | 파일 | 리소스 | |---|---|---| | (cluster) | 00 | Namespace lab-ext, lab-apps | | lab-ext | 10 | ConfigMap fake-dns-corefile / Deploy·Svc fake-dns | | lab-ext | 20 | ConfigMap ext-a-conf·ext-b-conf / Deploy ext-a·ext-b (**Service 없음** — 의도, 3절) | | lab-ext | 30 | Deploy ext-client | | lab-ext | (명령) | Secret ext-tls, ConfigMap lab-ca — ②단계에서 생성 | | lab-apps | 40 | Deploy mesh-client | | lab-apps | 50 | ConfigMap·Deploy·Svc web-a·web-b / **VS www-lab** | | lab-apps | 60 | **SE api-ext / VS api-ext-client** (client 쪽 리소스는 앱 ns) | | lab-apps | 65 | Sidecar default (REGISTRY_ONLY) | | lab-apps | (명령) | ConfigMap lab-ca — ②단계에서 생성 | | istio-system | 50 | **Gateway lab-ingress** (gateway workload ns 규율) | | istio-system | 60 | **Gateway lab-egress / VS api-ext-gateway** (동일 규율) | | istio-system | (명령) | Secret www-lab-tls — ②단계에서 생성 (credentialName은 workload ns에서 찾음) | ## 0. 반입 체크리스트 (폐쇄망 고유 작업 — 이것만 끝나면 나머지는 온라인과 동일) | 반입물 | 내용 | |---|---| | 이미지 6종 | `images.txt` — istio/pilot·proxyv2 1.30.0 + coredns/nginx/busybox/netshoot | | Helm chart 3종 | `helm pull istio/{base,istiod,gateway} --version 1.30.0` → tgz | | istioctl | 1.30.0 — 컨트롤플레인과 버전 일치 필수(1.27 클라이언트로 1.30 조회 시 진단 착시 실측) | 설치는 로컬 tgz 직접 지정(repo add 불필요), 순서는 base → istiod → gateway ×2 동일: ```bash helm upgrade --install istio-base ./base-1.30.0.tgz -n istio-system --create-namespace helm upgrade --install istiod ./istiod-1.30.0.tgz -n istio-system -f values-istiod.yaml helm upgrade --install istio-ingressgateway ./gateway-1.30.0.tgz -n istio-system -f values-ingress-gateway.yaml helm upgrade --install istio-egressgateway ./gateway-1.30.0.tgz -n istio-system -f values-egress-gateway.yaml ``` values 핵심 — 이미지 소스 교체 한 줄: ```yaml global: hub: registry.corp.example/docker.io/istio # docker.io/istio 대신 사내 미러 tag: 1.30.0 ``` ⚠ istiod의 `hub`는 **설치 시점이 아니라 주입 시점에 발화**함 — istiod가 정상이어도 주입 템플릿이 docker.io를 가리키면 이후 모든 신규 pod가 sidecar ImagePullBackOff로 죽음. 설치 직후 검증 1순위 = 주입된 이미지 주소 확인: `kubectl -n get pod -o jsonpath='{.items[*].spec.containers[*].image}'` 랩 manifest의 이미지도 폐쇄망에서는 apply 전에 미러 주소를 prepend함(이미지 참조가 전부 fully-qualified라 Harbor proxy-cache 레이아웃이 그대로 나옴): ```bash sed -E 's#^([[:space:]]*image:[[:space:]]*)#\1registry.corp.example/#' .yaml | kubectl apply -f - ``` ## 1. 구성 순서 — 파일 하나씩 apply (실행 → 확인) 작업 위치: `scenarios/60-airgap-lab/` ### ① 사설 CA와 인증서 — CA 하나, 서버 인증서 둘 ```bash openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ -keyout ca.key -out ca.crt -subj "/CN=Airgap Lab CA" # 외부 서버용 (ext-a/ext-b 공유 — SAN에 두 호스트) openssl req -newkey rsa:2048 -nodes -keyout ext.key -out ext.csr -subj "/CN=api.lab.external" openssl x509 -req -in ext.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -out ext.crt \ -extfile <(echo "subjectAltName=DNS:api.lab.external,DNS:blocked.lab.external") # ingress 종단용 openssl req -newkey rsa:2048 -nodes -keyout www.key -out www.csr -subj "/CN=www.lab.external" openssl x509 -req -in www.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -out www.crt \ -extfile <(echo "subjectAltName=DNS:www.lab.external") ``` 확인 — SAN이 비면 이후 curl이 전부 인증서 오류: ```bash openssl x509 -in ext.crt -noout -ext subjectAltName # 기대: api + blocked ``` ### ② namespace + secret/ConfigMap 인증서는 생성물이라 repo에 manifest로 커밋하지 않고 명령으로 주입함(YAML화 대상 아님): ```bash kubectl apply -f 00-namespaces.yaml kubectl -n lab-ext create secret tls ext-tls --cert=ext.crt --key=ext.key kubectl -n istio-system create secret tls www-lab-tls --cert=www.crt --key=www.key kubectl -n lab-ext create configmap lab-ca --from-file=ca.crt kubectl -n lab-apps create configmap lab-ca --from-file=ca.crt ``` `www-lab-tls`만 istio-system인 이유: `credentialName`은 Gateway 리소스의 ns가 아니라 **gateway workload의 ns**에서 secret을 찾음. ### ③ fake-dns — cluster DNS 발견 후 치환 apply ```bash CLUSTER_DNS=$(kubectl -n kube-system get svc coredns -o jsonpath='{.spec.clusterIP}') # 없으면 kube-dns echo $CLUSTER_DNS # 값부터 눈으로 확인 sed "s/__CLUSTER_DNS__/$CLUSTER_DNS/g" 10-fake-dns.yaml | kubectl apply -f - kubectl -n lab-ext rollout status deploy/fake-dns ``` ### ④ 외부 서버 모사 ```bash kubectl apply -f 20-ext-servers.yaml kubectl -n lab-ext rollout status deploy/ext-a kubectl -n lab-ext rollout status deploy/ext-b ``` ### ⑤ A 레코드 기록 — pod IP 조회 후 3줄 일괄 기록 Service가 없으므로 pod IP 직결(이유는 3절). **3줄을 한 번에** 기록(부분 기록 = 나머지 소실): ```bash IP_API=$(kubectl -n lab-ext get pod -l app=ext-a -o jsonpath='{.items[0].status.podIP}') IP_BLOCKED=$(kubectl -n lab-ext get pod -l app=ext-b -o jsonpath='{.items[0].status.podIP}') IP_INGRESS=$(kubectl -n istio-system get svc istio-ingressgateway -o jsonpath='{.spec.clusterIP}') kubectl -n lab-ext exec deploy/fake-dns -c writer -- sh -c \ "printf '%s api.lab.external\n%s blocked.lab.external\n%s www.lab.external\n' \ $IP_API $IP_BLOCKED $IP_INGRESS > /hosts/addn" kubectl -n lab-ext exec deploy/fake-dns -c writer -- cat /hosts/addn # 확인: 3줄 ``` ext pod 재시작 시 IP가 바뀜 → 이 단계만 재실행. ### ⑥ 클라이언트 — fake-dns IP + cluster domain 발견 후 치환 apply cluster domain은 하드코딩 금지 — coredns ConfigMap에서 발견(틀리면 sidecar 안 뜸): ```bash LABDNS_IP=$(kubectl -n lab-ext get svc fake-dns -o jsonpath='{.spec.clusterIP}') CLUSTER_DOMAIN=$(kubectl -n kube-system get cm coredns -o jsonpath='{.data.Corefile}' \ | grep -oE 'kubernetes[[:space:]]+[^ ]+' | awk '{print $2}') echo "$LABDNS_IP / $CLUSTER_DOMAIN" # 둘 다 값 확인 for f in 30-ext-client.yaml 40-mesh-client.yaml; do sed -e "s/__LABDNS_IP__/$LABDNS_IP/g" -e "s/__CLUSTER_DOMAIN__/$CLUSTER_DOMAIN/g" $f \ | kubectl apply -f - done ``` 확인 두 가지 — 이 랩에서 가장 잘 깨지는 단계: ```bash kubectl -n lab-apps get pod -l app=mesh-client # 기대: 2/2 (sidecar 기동 = istiod resolve 성공 증거) kubectl -n lab-ext exec deploy/ext-client -- dig +short www.lab.external # 기대: ingressgateway IP ``` 1/2에서 멈추면 ③의 forward 대상 또는 ⑥의 cluster domain부터 의심. ### ⑦ ingress 시나리오 ```bash kubectl apply -f 50-ingress.yaml istioctl analyze -n lab-apps && istioctl analyze -n istio-system # 경고 0 확인 후 트래픽(2절 T1·T2) ``` ### ⑧ egress + REGISTRY_ONLY 시나리오 ```bash kubectl apply -f 60-egress.yaml kubectl apply -f 65-sidecar-registry-only.yaml # 트래픽 전 설정 반영 확인 — SE cluster endpoint가 ⑤의 ext-a pod IP인가 istioctl proxy-config endpoints deploy/mesh-client -n lab-apps \ --cluster "outbound|443||api.lab.external" ``` ## 2. 트래픽 테스트와 합격선 ```bash # T1. ingress host/path 분기 (평문) — 기대: / -> web-a, /b -> web-b kubectl -n lab-ext exec deploy/ext-client -- curl -sS http://www.lab.external/ kubectl -n lab-ext exec deploy/ext-client -- curl -sS http://www.lab.external/b # T2. ingress TLS termination + 사설 CA 검증 — 기대: 200 web-a kubectl -n lab-ext exec deploy/ext-client -- \ curl -sS --cacert /etc/lab-ca/ca.crt https://www.lab.external/ # T3. egress gateway 경유 — 기대: ext-a kubectl -n lab-apps exec deploy/mesh-client -c netshoot -- \ curl -sS --cacert /etc/lab-ca/ca.crt https://api.lab.external/ # T4. REGISTRY_ONLY 차단 — 기대: 실패(connection reset) = PASS kubectl -n lab-apps exec deploy/mesh-client -c netshoot -- \ curl -sS --cacert /etc/lab-ca/ca.crt https://blocked.lab.external/ ``` 경유·반영 심층 확인: ```bash istioctl proxy-config listeners deploy/mesh-client -n lab-apps --port 443 kubectl -n istio-system logs deploy/istio-egressgateway --tail=20 kubectl -n istio-system logs deploy/istio-ingressgateway --tail=20 ``` ## 3. 설계 결정 요약 (왜 이렇게 했는가 — 상세는 각 manifest 머리 주석) - **ns 2개 분리**: mesh 안/밖 경계 자체가 테스트 대상이라 ns 경계로 명시. blast radius 한정. - **Gateway 리소스는 gateway workload ns(istio-system)로 통일**: lab-ingress·lab-egress 모두. PILOT_SCOPE_GATEWAY_TO_NAMESPACE 대비 + dual-gateway 랩과 동일 규율. 다른 ns의 VS는 `istio-system/lab-ingress` 정규화 형식으로 참조. - **ext 서버에 Service 없음**: Service ClusterIP로 SE 호스트가 resolve되면 per-VIP 리스너가 SNI 캐치올보다 먼저 매치 → curl 200인 채 실험 무효(50-dns 랩 실측 함정). pod IP 직결. 대가로 pod 재시작 시 ⑤단계 재실행. - **blocked.lab.external 뒤에도 살아있는 서버**: 실패 원인을 "sidecar 정책" 하나로 고립. - **REGISTRY_ONLY를 Sidecar 리소스(ns 스코프)로**: 전역 meshConfig는 공유 클러스터의 다른 워크로드 외부 호출까지 차단함. - **egress는 공용 istio-egressgateway 사용**: 폐쇄망 첫 검증은 추가 gateway 반입·배포 없이. 전용 gateway 패턴이 필요하면 20-egress/dual-gateway 참조. - **VS/DR destination은 cluster.local 고정**: Istio service registry는 kubelet clusterDomain과 무관하게 *.svc.cluster.local로 등록(pod resolv.conf의 search 도메인과 별개 축). - **secret/CA는 manifest가 아니라 명령으로**: 인증서는 생성물이라 repo 커밋 대상이 아님(①·②). ## 4. 함정 (실측 기반) 1. `global.hub` 오설정은 주입 시점 발화 — 위 0절. 2. cluster domain 하드코딩 금지 — dnsConfig searches가 틀리면 istiod resolve 실패 → sidecar 안 뜸(50-dns 랩 2026-07-01 실발생). ⑥단계에서 coredns cm으로 자동 발견. 3. fake-dns Corefile 변경은 rollout restart 필요(존파일 /hosts/addn만 reload 2s 자동). 4. /hosts/addn은 항상 3줄 전체 재기록 — 한 줄만 쓰면 나머지 레코드 소실. 5. ext pod 재시작 = pod IP 변경 → A 레코드 낡음 → ⑤단계 재실행. 6. netshoot:latest는 반입 시점에 태그 고정(images.txt 주석). ## 5. 정리 (위험 작업 — CLAUDE.md §6 승인 후 수동 실행) ```bash kubectl delete -f 65-sidecar-registry-only.yaml kubectl delete -f 60-egress.yaml # istio-system의 Gateway/VS 포함 kubectl delete -f 50-ingress.yaml # istio-system의 Gateway 포함 kubectl -n istio-system delete secret www-lab-tls kubectl delete ns lab-apps lab-ext ``` --- 참고: `scripts/airgap-lab-setup.sh` 는 위 ①~⑧과 동일한 절차의 자동화 버전(반복 재구축용). 처음 구성은 본 문서대로 하나씩 apply 하며 확인하는 것을 기본으로 함.