Istio Service Mesh
K8s의 확장 — NetworkPolicy(L3/L4)와 Istio(L7)를 함께 사용해 네트워크 격리와 트래픽 제어를 구성한다.
K8s NetworkPolicy만으로는 HTTP 경로·헤더·JWT 같은 L7 조건으로 트래픽을 제어할 수 없다. Istio는 각 Pod 옆에 Envoy sidecar를 주입해 모든 트래픽을 가로채고, L7 레벨에서 인증·인가·암호화·필터링을 처리한다.
Istio 주요 Kind
| Kind | API Group | 역할 | Playball 적용 |
|---|---|---|---|
Gateway | networking.istio.io | 외부 트래픽 수신 진입점. 도메인·포트·TLS 설정 | HTTPS 443 / HTTP 80 → HTTPS 리다이렉트. 도메인별 shared gateway |
VirtualService | networking.istio.io | Gateway에서 받은 트래픽을 어떤 서비스로 보낼지 라우팅 규칙 정의 | 경로(prefix) 기반으로 api-gateway, grafana, kafka-ui, load-test 등 라우팅 |
DestinationRule | networking.istio.io | 서비스로 나가는 트래픽 정책. mTLS 모드·로드밸런싱·서킷브레이커 설정 | *.local 전체 ISTIO_MUTUAL. messaging/data 네임스페이스는 DISABLE |
PeerAuthentication | security.istio.io | 서비스 간(sidecar↔sidecar) mTLS 인증 모드 설정. STRICT이면 TLS 없는 요청 거부 | mesh 전체 STRICT. 메트릭 스크래핑 포트(9090)만 포트 단위 DISABLE |
RequestAuthentication | security.istio.io | JWT 토큰 검증 규칙. 토큰이 있으면 서명·만료 검증, 없으면 그냥 통과 (차단은 AuthorizationPolicy가 담당) | Kakao OAuth / 자체 auth-guard JWT. 현재 enabled: false — API Gateway 서비스가 직접 처리 |
AuthorizationPolicy | security.istio.io | 요청 인가 정책. ALLOW/DENY 액션으로 경로·IP·JWT principal 조건 설정 | 내부 경로 차단(DENY), 보호 경로 JWT 필수(DENY), Admin 도구 IP 화이트리스트(DENY) |
EnvoyFilter | networking.istio.io | Envoy 프록시 설정을 직접 패치. Istio CRD로 표현 못하는 세밀한 필터 추가 | WAF(Lua), Origin Verify(Lua), Rate Limit Local/Global, ext_authz — 모두 EnvoyFilter로 주입 |
Sidecar | networking.istio.io | 특정 워크로드의 sidecar가 트래픽을 허용할 이그레스 범위를 제한 | 앱 서비스별 egress 허용 목적지 명시 (같은 NS, istio-system, data) |
ServiceEntry | networking.istio.io | mesh 외부 서비스(외부 도메인·IP)를 Istio가 알 수 있도록 등록 | Kakao OAuth(kauth.kakao.com) 등 외부 API 허용 |
Telemetry | telemetry.istio.io | 트레이싱 샘플링·프로바이더, 메트릭 설정 | Grafana Tempo로 분산 트레이스 전송, 샘플링 10% |
PeerAuthentication은 sidecar 간 mTLS(서비스↔서비스 암호화)를 제어하고,RequestAuthentication은 HTTP 요청 헤더의 JWT 토큰 유효성을 검사한다. 두 개는 다른 레이어를 담당한다.
왜 이 Kind에서 설정하지? — 자주 헷갈리는 분리 패턴
Istio는 하나의 논리적 기능을 의도적으로 여러 CRD에 나눠 담는다. "왜 두 곳에서 설정해야 하지?"라는 의문이 생기는 지점들을 정리한다.
JWT 차단이 두 단계인 이유
JWT를 완전히 강제하려면 두 개의 CRD가 함께 동작해야 한다.
| 단계 | Kind | 하는 일 |
|---|---|---|
| 1 | RequestAuthentication | 토큰이 있으면 서명·만료를 검증한다. 토큰이 아예 없으면 그냥 통과시킨다. |
| 2 | AuthorizationPolicy | request.auth.principal 값이 없는 요청(= 토큰 없음)을 DENY한다. |
RequestAuthentication 혼자로는 토큰 없는 요청을 막을 수 없다. "검증기"와 "게이트키퍼"를 분리한 설계다. 덕분에 "토큰이 있으면 검증하되, 없어도 허용"하는 공개 API와 "토큰 필수" 보호 API를 같은 Gateway에서 경로별로 다르게 운영할 수 있다.
mTLS 설정이 두 곳인 이유
| Kind | 담당 방향 | 하는 일 |
|---|---|---|
PeerAuthentication | 수신(서버) 측 | "나한테 오는 요청은 mTLS여야 해" — sidecar가 plain HTTP를 받으면 거부 |
DestinationRule | 송신(클라이언트) 측 | "저 서비스로 보낼 때 mTLS로 보내" — sidecar가 업스트림에 TLS를 붙여서 전송 |
둘 다 설정해야 mesh 안에서 양방향으로 mTLS가 완성된다. PeerAuthentication만 있으면 서버는 mTLS를 요구하는데 클라이언트는 plain으로 보내서 연결이 실패하고, DestinationRule만 있으면 클라이언트가 mTLS로 보내는데 서버가 거부하지 않아 실질적 강제가 없다.
CORS 설정 위치
CORS는 AuthorizationPolicy가 아니라 VirtualService의 corsPolicy에서 설정한다. VirtualService는 라우팅 레이어라 요청이 서비스에 도달하기 전에 허용 Origin을 판단할 수 있다. AuthorizationPolicy는 인가(누가 접근할 수 있나)를 다루지, HTTP CORS 헤더를 다루지 않는다.
# VirtualService에서 CORS 설정
spec:
http:
- corsPolicy:
allowOrigins:
- exact: "https://playball.one"
allowMethods: [GET, POST, PUT, DELETE]
allowHeaders: [Authorization, Content-Type]
EnvoyFilter가 필요한 시점
Istio CRD(Gateway, VirtualService, AuthorizationPolicy 등)는 Envoy의 기능 중 자주 쓰이는 것만 추상화해서 제공한다. Lua 스크립트 실행, Rate Limit 세밀 설정, ext_authz 커스텀 클러스터 등록처럼 Istio CRD로 표현할 수 없는 것은 EnvoyFilter로 Envoy 설정을 직접 패치한다. 강력하지만 Envoy 내부 구조를 알아야 해서 복잡하고, Istio 버전 업그레이드 시 호환성 확인이 필요하다.
Sidecar와 NetworkPolicy의 역할 차이
| NetworkPolicy | Sidecar | |
|---|---|---|
| 동작 레이어 | L3/L4 (IP·포트) | L7 (HTTP, gRPC 호스트명) |
| 설정 단위 | 네임스페이스·Pod 셀렉터 | 워크로드(app 레이블) |
| 목적 | "어느 Pod에서 오는 트래픽만 허용" | "어느 서비스로 나가는 트래픽만 허용" |
두 개는 다른 레이어에서 동시에 작동한다. NetworkPolicy가 IP 레벨에서 1차로 걸러내고, Sidecar가 서비스 이름 기준으로 이그레스를 추가 제한한다.
K8s NetworkPolicy — L3/L4 격리
NetworkPolicy는 IP·포트·네임스페이스 단위의 기본 격리 레이어다. Playball은 default-deny-all을 기본으로 깔고, 필요한 방향만 명시적으로 허용한다.
기본 차단 정책
# common-charts/infra/network-policies/templates/default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: {{ $ns }}
spec:
podSelector: {} # 네임스페이스 내 모든 Pod
policyTypes:
- Ingress
- Egress # Ingress·Egress 모두 차단
모든 네임스페이스에 이 정책이 먼저 적용되고, 아래 허용 정책에 매칭된 트래픽만 통과한다.
네임스페이스별 허용 규칙
# common-charts/infra/network-policies/templates/allow-policies.yaml
# staging-webs 예시
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: istio-system # IngressGateway 허용
- podSelector: {} # 같은 네임스페이스 내부 통신
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: data # Kafka, Redis
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: staging-ai # AI 서비스
# DNS 항상 허용
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
네임스페이스별 허용 관계:
| 네임스페이스 | Ingress 허용 출처 | Egress 허용 목적지 |
|---|---|---|
| staging-webs | istio-system, 자기 자신 | data, staging-ai, 자기 자신 |
| staging-ai | istio-system, staging-webs, 자기 자신 | data, staging-webs, messaging, 자기 자신 |
| data | staging-webs, staging-ai, loadtest-webs, loadtest-ai, monitoring | 자기 자신 |
| monitoring | istio-system, 자기 자신 | 모든 네임스페이스 (Prometheus 스크래핑) |
Envoy 필터 체인 — 읽는 순서
외부에서 들어온 요청은 Istio IngressGateway의 Envoy를 통과하면서 아래 순서대로 처리된다. 앞 단계에서 차단되면 이후 단계는 실행되지 않는다.
요청 진입
│
▼
① WAF (EnvoyFilter Lua)
│ SQL Injection / XSS / Path Traversal / SSRF / Log4Shell 등 패턴 검사
│ 탐지 시 → 403 즉시 반환, autoBlocklist 카테고리는 authz-adapter로 IP 등록
▼
② Origin Verify (EnvoyFilter Lua)
│ X-Origin-Verify 헤더 값 검증 — ALB SG(CloudFront IP만 허용)와 이중 방어
▼
③ Rate Limit — Local (EnvoyFilter)
│ Envoy 내장 토큰 버킷. IP당·경로별 임계치 초과 시 → 429
▼
④ Rate Limit — Global (EnvoyFilter)
│ Redis 기반 분산 카운터. 여러 Gateway 인스턴스 합산 → 429
▼
⑤ ext_authz (EnvoyFilter)
│ AI Defense gRPC 어댑터 호출. 행동 점수·IP 블랙리스트 판단
│ failOpen: true — AI 서버 장애 시 통과 허용
▼
⑥ AuthorizationPolicy
│ 경로 차단(deny-internal-paths), JWT 요구, Admin 도구 IP 화이트리스트
▼
⑦ mTLS (서비스 간 통신)
PeerAuthentication STRICT — sidecar 간 상호 인증·암호화
① WAF — EnvoyFilter Lua
# templates/envoyfilter-waf.yaml
applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
subFilter:
name: envoy.filters.http.router
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.lua
values에서 탐지 패턴 개별 토글:
# values-istio-security.yaml
mode: block # block | detect
patterns:
sqlInjection: { enabled: true }
xss: { enabled: true }
pathTraversal: { enabled: true }
commandInjection:{ enabled: true }
ldapInjection: { enabled: true }
xxeInjection: { enabled: true }
ssrf: { enabled: true }
log4shell: { enabled: true }
headerInjection: { enabled: true }
botScanner: { enabled: true }
# 고신뢰 카테고리: 단일 탐지 시 즉시 IP 차단
autoBlocklist:
categories: [LOG4SHELL, COMMAND_INJECTION, XXE_INJECTION, LDAP_INJECTION]
# 임계값 카테고리: 5분 내 3회 탐지 시 차단 (FP 방지)
threshold:
windowSeconds: 300
count: 3
categories: [XSS, PATH_TRAVERSAL, SSRF, HEADER_INJECTION, BOT_SCANNER]
② Origin Verify — EnvoyFilter Lua
CloudFront가 요청에 X-Origin-Verify 헤더를 추가한다. ALB SG로 CloudFront IP만 허용하고 있지만, 헤더 검증으로 이중 방어한다.
# values-istio-security.yaml
originVerify:
enabled: true
headerName: "X-Origin-Verify"
headerValue: "playball-staging-origin-verify-2026"
excludePaths:
- "/healthz"
- "/ready"
- "/actuator/health"
③④ Rate Limit — Local + Global
Local Rate Limit은 각 Envoy 인스턴스 내부 토큰 버킷이고, Global은 Redis 기반으로 여러 Gateway Pod 합산값을 기준으로 판단한다.
# Local: Envoy 내장 토큰 버킷
rateLimiting:
local:
default:
tokensPerFill: 100 # 전체: 초당 100
fillInterval: 1s
perClient:
tokensPerFill: 50 # IP당: 초당 50
perRoute:
routes:
- path: "/auth/"
tokensPerFill: 10 # 인증: 초당 10
- path: "/payment/"
tokensPerFill: 5 # 결제: 초당 5
- path: "/signup"
tokensPerFill: 3 # 회원가입: 초당 3
# Global: Redis 기반 분산 카운터
global:
domain: "playball-staging-ratelimit"
descriptors:
- key: remote_address
rateLimit:
requestsPerUnit: 300
unit: minute # IP당 분당 300
- key: path
value: "/auth/kakao/login"
descriptors:
- key: remote_address
rateLimit:
requestsPerUnit: 10
unit: minute # 로그인: IP당 분당 10
⑤ ext_authz — AI Defense 연동
티켓팅 핵심 경로(대기열 진입, 좌석 선택, Hold 생성 등)에만 적용한다. AI Defense가 행동 패턴을 분석해 정상/봇 여부를 판단한다.
# templates/envoyfilter-ext-authz.yaml
extAuthz:
service:
host: staging-authz-adapter.staging-ai.svc.cluster.local
port: 9001
connectTimeout: "0.5s"
timeout: "0.8s"
failOpen: true # AI 서버 장애 시 통과 허용
includePaths:
- { method: POST, pattern: "^/queue/matches/[0-9]+/enter$", eventType: QUEUE_ENTER }
- { method: POST, pattern: "^/seat/matches/[0-9]+/seat-holds$", eventType: SEAT_HOLDS }
- { method: POST, pattern: ".../recommendations/blocks/.../assign$", eventType: ASSIGN_HOLD }
- { method: GET, pattern: "^/seat/matches/[0-9]+/seat-groups$", eventType: SEAT_ENTRY }
⑥ AuthorizationPolicy
3개의 정책이 함께 동작한다.
# 1. 내부 경로 차단 — Prometheus·health 등 외부 노출 차단
kind: AuthorizationPolicy
metadata:
name: deny-internal-paths
spec:
action: DENY
rules:
- to:
- operation:
paths: ["/api/prometheus*", "/metrics*", "/health*", "/actuator/health*"]
# 2. Admin 도구 IP 화이트리스트 — 팀원 IP 이외 접근 차단
kind: AuthorizationPolicy
metadata:
name: admin-tools-ip-whitelist
spec:
action: DENY
rules:
- from:
- source:
notRemoteIpBlocks: ["59.x.x.x/32", ...] # 팀원 IP 목록
to:
- operation:
hosts: ["grafana.*", "kafka-ui.*", "cloudbeaver.*", ...]
# 3. JWT 인증 (jwtAuth.enabled: false — API Gateway에서 처리)
# API Gateway 서비스가 JWT 검증을 담당하므로 Istio 레이어 JWT는 비활성화
⑦ mTLS — PeerAuthentication + DestinationRule
# templates/peerauthentication-mtls.yaml
# Mesh 전체 기본: STRICT
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: STRICT
# templates/destinationrule-mtls.yaml
# cluster.local 내 모든 서비스: ISTIO_MUTUAL
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: default-mtls
namespace: istio-system
spec:
host: "*.local"
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
Istio sidecar가 없는 네임스페이스로의 트래픽은 mTLS를 끈다. 켜두면 Envoy가 TLS ClientHello를 일반 메시지로 보내 프로토콜이 불일치한다.
# messaging (Kafka), data (ClickHouse) — sidecar 없음
nonMeshNamespaces:
- namespace: messaging
reason: "Kafka 브로커에 Istio sidecar 없음"
- namespace: data
reason: "ClickHouse에 Istio sidecar 없음"
메트릭 스크래핑 포트만 예외 처리한다. Prometheus가 actuator 포트(9090)를 plain HTTP로 scrape하므로, 해당 포트만 DISABLE로 열어둔다.
# App 서비스 메트릭 포트 예외 (staging-webs 전체)
portLevelMtls:
9090:
mode: DISABLE # Prometheus scrape 허용
Sidecar Egress 제한
각 서비스가 통신할 수 있는 목적지를 명시적으로 제한한다. 선언하지 않은 외부 호스트로의 트래픽은 차단된다.
# templates/sidecar-egress.yaml
apiVersion: networking.istio.io/v1
kind: Sidecar
metadata:
name: egress-auth-guard
namespace: staging-webs
spec:
workloadSelector:
labels:
app: staging-auth-guard
egress:
- hosts: ["./*"] # 같은 네임스페이스
- hosts: ["istio-system/*"]
- hosts: ["data/*"] # Redis, DB
# 외부 도메인은 ServiceEntry로 명시 후 추가
외부 API(Kakao OAuth 등)는 ServiceEntry로 등록한다.
# templates/serviceentry-egress.yaml
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
name: egress-kakao-auth
spec:
hosts: ["kauth.kakao.com"]
location: MESH_EXTERNAL
ports:
- number: 443
protocol: TLS
resolution: DNS
옵저버빌리티 연동
# common-charts/infra/istio/istiod/templates/telemetry-tracing.yaml
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
spec:
tracing:
- providers:
- name: tempo # Grafana Tempo로 분산 트레이스 전송
randomSamplingPercentage: 10.0
Envoy 메트릭은 PodMonitor로 수집해 Prometheus → Grafana 대시보드에서 확인한다.
# common-charts/infra/istio/istiod/templates/podmonitor-envoy.yaml
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: envoy-stats
spec:
podMetricsEndpoints:
- path: /stats/prometheus
port: http-envoy-prom