- Published on
Istio 보안 실전 — mTLS, AuthorizationPolicy, 그리고 제로트러스트 메시
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- 메시 보안 모델 — 아이덴티티가 모든 것의 기초
- PeerAuthentication — mTLS 모드와 STRICT 전환 전략
- AuthorizationPolicy — 최소 권한 인가 설계
- RequestAuthentication — JWT 최종 사용자 인증
- 외부 인가 — ext-authz로 OPA 연동
- 커스텀 CA 통합 — cert-manager와 사내 PKI
- 정책 디버깅 — 거부 로그와 dry-run
- 컴플라이언스 관점 — 메시 보안이 채워주는 것
- 성능 영향 — 측정 기준으로 말하기
- 도입 로드맵 — 관찰에서 제로트러스트까지
- 체크리스트
- 마치며
- 참고 자료
들어가며
서비스 메시를 도입하는 두 번째로 큰 이유는 보안입니다. 쿠버네티스 클러스터 내부는 흔히 "신뢰할 수 있는 네트워크"로 취급되지만, 실제로는 하나의 파드만 탈취당해도 클러스터 내부의 모든 서비스에 평문으로 접근할 수 있는 평평한 네트워크인 경우가 많습니다. 제로트러스트의 출발점은 이 가정을 뒤집는 것입니다. 내부 네트워크도 신뢰하지 않고, 모든 서비스 간 통신에 암호화와 인증과 인가를 요구하는 것이죠.
문제는 이것을 애플리케이션 코드로 구현하려면 모든 서비스에 TLS 인증서 발급·갱신 로직, 상대방 검증 로직, 권한 검사 로직을 넣어야 한다는 점입니다. 수십 개 서비스, 여러 언어 스택에서 이를 일관되게 유지하는 것은 사실상 불가능합니다. Istio는 이 세 가지(암호화, 워크로드 인증, 인가)를 인프라 계층으로 끌어내려 코드 수정 없이 일괄 적용합니다.
이 글은 Istio 보안의 동작 원리(SPIFFE 아이덴티티, istiod CA, 인증서 자동 갱신)에서 시작해, PeerAuthentication의 단계적 STRICT 전환, AuthorizationPolicy의 최소 권한 설계, RequestAuthentication의 JWT 검증, OPA 외부 인가 연동, 커스텀 CA 통합, 그리고 정책 디버깅과 도입 로드맵까지 실전 순서로 정리합니다. 예제는 사이드카 모드 기준이며, Ambient 모드에서는 L4 보안(mTLS)은 ztunnel이, L7 인가는 waypoint가 담당한다는 차이만 있고 API는 동일합니다.
메시 보안 모델 — 아이덴티티가 모든 것의 기초
SPIFFE 아이덴티티
Istio 보안의 핵심은 "워크로드마다 암호학적으로 검증 가능한 신원을 부여한다"는 것입니다. 이 신원은 SPIFFE(Secure Production Identity Framework For Everyone) 표준 형식을 따릅니다.
spiffe://cluster.local/ns/payments/sa/payments-api
------------- -------- ------------
트러스트 도메인 네임스페이스 서비스어카운트
즉 Istio에서 워크로드의 신원은 IP 주소도, 파드 이름도 아닌 쿠버네티스 서비스어카운트입니다. IP는 파드가 재시작하면 바뀌고 스푸핑이 가능하지만, 서비스어카운트 기반 SPIFFE ID는 인증서에 새겨져 mTLS 핸드셰이크에서 암호학적으로 검증됩니다. 뒤에서 다룰 AuthorizationPolicy의 principals 필드가 바로 이 SPIFFE ID를 참조합니다.
istiod CA와 인증서 발급·갱신 흐름
istiod는 메시의 인증 기관(CA) 역할을 겸합니다. 워크로드 인증서가 발급되고 갱신되는 흐름을 그림으로 보겠습니다.
+----------------------------------------------------------------------+
| 컨트롤 플레인 |
| +----------------------------------------------------------------+ |
| | istiod (내장 CA) | |
| | - 루트/중간 CA 키 보관 (istio-ca-secret 또는 cacerts) | |
| | - CSR 서명, SPIFFE ID를 SAN에 기록 | |
| +----------------------------------------------------------------+ |
| ^ | |
| | 3. CSR 제출 | 4. 서명된 인증서 |
| | (서비스어카운트 토큰으로 인증) | (기본 24시간 유효) |
+--------|----------------------------------------|--------------------+
| v
+--------|----------------------------------------------------------+
| 워크로드 파드 |
| +--------------------------+ +------------------------------+ |
| | istio-agent (pilot-agent) |<-->| Envoy 사이드카 | |
| | 1. 키 쌍 생성 | SDS | - 발급받은 인증서로 | |
| | 2. CSR 생성 | | mTLS 핸드셰이크 | |
| | 5. SDS로 Envoy에 전달 | | - 상대 인증서의 SPIFFE ID 검증| |
| | 6. 만료 전 자동 재발급 | +------------------------------+ |
| +--------------------------+ |
+--------------------------------------------------------------------+
운영 관점에서 중요한 포인트는 다음과 같습니다.
- 개인키는 파드 밖으로 나가지 않습니다. istio-agent가 파드 안에서 키 쌍을 만들고 CSR만 istiod에 보냅니다.
- 인증서는 기본 24시간 유효이며, 만료 전(기본적으로 수명의 절반 시점부터) istio-agent가 자동으로 재발급받습니다. 사람이 갱신 작업을 할 일이 없습니다.
- Envoy는 SDS(Secret Discovery Service)로 인증서를 받으므로, 인증서 갱신 시 커넥션 드레인이나 파드 재시작이 필요 없습니다.
- 인증서의 SAN(Subject Alternative Name)에 SPIFFE ID가 들어가며, 이것이 인가 정책의 매칭 대상입니다.
신뢰 사슬과 트러스트 도메인
기본 설치에서 istiod는 자체 서명 루트 CA를 생성합니다(istio-system 네임스페이스의 istio-ca-secret). 멀티클러스터 메시에서는 모든 클러스터가 같은 루트를 공유해야 클러스터 간 mTLS가 성립하므로, 운영 환경에서는 처음부터 공통 루트 CA를 cacerts 시크릿으로 주입하는 것이 정석입니다. 이 부분은 뒤의 커스텀 CA 섹션에서 다시 다룹니다.
PeerAuthentication — mTLS 모드와 STRICT 전환 전략
세 가지 모드
PeerAuthentication은 "이 워크로드가 들어오는 트래픽에 mTLS를 어떻게 요구할 것인가"를 정합니다.
| 모드 | 동작 | 사용 시점 |
|---|---|---|
| PERMISSIVE | mTLS와 평문 모두 수락 | 마이그레이션 기간 (기본값) |
| STRICT | mTLS만 수락, 평문 거부 | 목표 상태 |
| DISABLE | mTLS 비활성 | 외부 TLS 종료 장비 뒤 등 예외 |
기본값이 PERMISSIVE라는 점이 중요합니다. 사이드카를 주입하면 메시 내부 트래픽은 자동으로 mTLS가 되지만, 사이드카 없는 파드나 메시 밖에서 오는 평문 트래픽도 여전히 수락됩니다. 즉 PERMISSIVE는 "암호화는 되고 있지만 강제는 아닌" 상태이며, 평문 경로가 남아 있는 한 제로트러스트라고 부를 수 없습니다.
적용 범위: 메시 전역, 네임스페이스, 워크로드
# 1) 메시 전역 기본값 — istio-system(루트 네임스페이스)에 둔다
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: STRICT
---
# 2) 네임스페이스 단위 — 해당 네임스페이스의 모든 워크로드에 적용
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: payments
spec:
mtls:
mode: STRICT
---
# 3) 워크로드 단위 — selector로 특정 워크로드만, 포트별 예외도 가능
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: legacy-metrics-exception
namespace: payments
spec:
selector:
matchLabels:
app: legacy-batch
mtls:
mode: STRICT
portLevelMtls:
9090: # 메시 밖 Prometheus가 긁어가는 포트만 평문 허용
mode: PERMISSIVE
우선순위는 워크로드 단위가 네임스페이스 단위를, 네임스페이스 단위가 메시 전역을 이깁니다. 좁은 범위가 항상 승리한다고 기억하시면 됩니다.
PERMISSIVE에서 STRICT로 — 네임스페이스 단계 전환
전체 메시를 한 번에 STRICT로 바꾸는 것은 도박입니다. 사이드카 없는 클라이언트(메시 밖 cron, 레거시 VM, kubelet 헬스체크가 아닌 커스텀 프로브 등)가 하나라도 있으면 그 경로가 즉시 끊어집니다. 권장 절차는 다음과 같습니다.
단계 0: 현황 관찰 (PERMISSIVE 유지)
- Grafana/Kiali에서 평문 트래픽 비율 확인
- 메트릭: istio_requests_total의
connection_security_policy 라벨 (mutual_tls / none)
- none이 잡히는 출발지를 전수 조사
→ 사이드카 미주입 파드인가, 메시 밖 클라이언트인가
단계 1: 평문 출발지 제거
- 사이드카 미주입 네임스페이스에 주입 라벨 추가
- 메시 밖 클라이언트는 메시로 편입하거나 ingress gateway 경유로 변경
단계 2: 비핵심 네임스페이스부터 STRICT 적용
- dev → staging → 운영 비핵심 → 운영 핵심 순서
- 네임스페이스당 PeerAuthentication 하나로 명시적 적용
- 적용 후 5xx/연결 오류 대시보드를 일정 기간 관찰
단계 3: 메시 전역 기본값을 STRICT로
- istio-system의 default PeerAuthentication을 STRICT로
- 남은 예외는 portLevelMtls 또는 워크로드 단위로 좁게 명시
단계 4: 예외 목록을 주기적으로 재심사
- DISABLE/PERMISSIVE 예외는 만료일을 정해 관리
전환 중 평문 트래픽이 어디서 오는지 확인하는 명령은 다음과 같습니다.
# 특정 워크로드의 mTLS 적용 상태와 정책 출처 확인
istioctl x describe pod payments-api-6c9f7d-abcde -n payments
# 네임스페이스의 인증 정책 충돌/누락 점검
istioctl analyze -n payments
# Prometheus에서 평문 요청 추적 (none이 0이 되어야 STRICT 가능)
# istio_requests_total{connection_security_policy="none"}
AuthorizationPolicy — 최소 권한 인가 설계
mTLS가 "누구인지"를 증명한다면, AuthorizationPolicy는 "그래서 무엇을 할 수 있는지"를 정합니다. 평가 규칙을 먼저 정확히 이해해야 합니다.
AuthorizationPolicy 평가 순서 (요청 하나당):
1. CUSTOM 정책 평가 → 외부 인가기가 거부하면 즉시 거부
2. DENY 정책 평가 → 하나라도 매칭되면 즉시 거부
3. ALLOW 정책 평가
- 대상 워크로드에 ALLOW 정책이 하나도 없으면 → 허용 (기본 개방)
- ALLOW 정책이 하나라도 있으면 → 매칭되는 것이 있어야 허용,
없으면 거부 (기본 거부로 전환)
핵심은 마지막 줄입니다. ALLOW 정책이 존재하는 순간 그 워크로드는 화이트리스트 모드가 됩니다. 이 성질을 이용해 기본 거부를 구성합니다.
기본 거부 — 제로트러스트의 출발점
# 빈 spec의 ALLOW 정책 = 아무것도 허용하지 않음 = 네임스페이스 기본 거부
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: deny-all
namespace: payments
spec: {}
빈 spec은 "ALLOW 정책은 존재하지만 매칭 규칙이 없다"는 뜻이므로 모든 요청이 거부됩니다. 이 상태에서 필요한 경로만 명시적으로 열어가는 것이 최소 권한 설계입니다.
서비스 간 최소 권한 — 호출 그래프대로만 허용
주문 서비스만 결제 API를 호출할 수 있어야 한다면 이렇게 작성합니다.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: payments-api-allow
namespace: payments
spec:
selector:
matchLabels:
app: payments-api
action: ALLOW
rules:
- from:
- source:
principals:
- cluster.local/ns/commerce/sa/orders-api # SPIFFE ID 기반
to:
- operation:
methods: ["POST"]
paths: ["/v1/charges", "/v1/refunds"]
- from:
- source:
principals:
- cluster.local/ns/observability/sa/prometheus
to:
- operation:
methods: ["GET"]
paths: ["/metrics"]
principals는 mTLS 인증서의 SPIFFE ID와 매칭되므로, 이 정책이 동작하려면 mTLS가 활성화되어 있어야 합니다. PERMISSIVE 상태에서 평문으로 들어온 요청은 principal이 비어 있어 principals 조건에 매칭되지 않는다는 점을 기억하세요. STRICT 전환이 인가 설계의 전제 조건인 이유입니다.
메서드·경로 제한과 DENY의 활용
ALLOW로 화이트리스트를 만들되, 운영자 관점에서 "무슨 일이 있어도 막아야 하는 것"은 DENY로 한 겹 더 깔아두는 것이 안전합니다. DENY는 ALLOW보다 먼저 평가되므로 ALLOW 정책 실수를 방어하는 안전망이 됩니다.
# 관리용 경로는 어떤 출발지에서도 메시 내부 호출 금지
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: deny-admin-paths
namespace: payments
spec:
selector:
matchLabels:
app: payments-api
action: DENY
rules:
- to:
- operation:
paths: ["/admin/*", "/internal/*"]
---
# 특정 네임스페이스 전체에서 오는 트래픽 차단 (예: 샌드박스 격리)
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: deny-from-sandbox
namespace: payments
spec:
action: DENY
rules:
- from:
- source:
namespaces: ["sandbox"]
조건(when)을 이용한 세밀한 제어
# ingress gateway를 거쳐 들어온 외부 트래픽만 허용하고,
# 특정 헤더가 있는 요청만 통과시키는 예
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: frontend-via-gateway-only
namespace: commerce
spec:
selector:
matchLabels:
app: storefront
action: ALLOW
rules:
- from:
- source:
principals:
- cluster.local/ns/istio-ingress/sa/ingress-gateway
when:
- key: request.headers[x-api-version]
values: ["v1", "v2"]
when 조건으로는 요청 헤더 외에도 source.ip, connection.sni, request.auth.claims(JWT 클레임) 등을 쓸 수 있습니다. 특히 JWT 클레임 조건은 다음 섹션의 RequestAuthentication과 결합됩니다.
RequestAuthentication — JWT 최종 사용자 인증
mTLS의 principal이 "어느 워크로드가 호출했는가"라면, JWT는 "어느 최종 사용자(또는 어느 클라이언트 앱)의 요청인가"를 나타냅니다. RequestAuthentication은 JWT의 서명·발급자·수명을 검증하고, 검증된 클레임을 인가 조건으로 노출합니다.
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
name: jwt-auth
namespace: commerce
spec:
selector:
matchLabels:
app: storefront-api
jwtRules:
- issuer: "https://idp.example.com/" # iss 클레임과 일치해야 함
audiences:
- "storefront-api" # aud 클레임 검증
jwksUri: "https://idp.example.com/.well-known/jwks.json"
forwardOriginalToken: true # 백엔드로 토큰 전달
여기서 가장 많이 하는 오해: RequestAuthentication만 만들면 "JWT 없는 요청도 통과"합니다. 이 리소스는 "JWT가 있다면 유효해야 한다"만 강제하기 때문입니다. JWT를 필수로 만들려면 AuthorizationPolicy를 결합해야 합니다.
# 유효한 JWT principal이 있는 요청만 허용 → 토큰 없으면 거부
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: require-jwt
namespace: commerce
spec:
selector:
matchLabels:
app: storefront-api
action: ALLOW
rules:
- from:
- source:
requestPrincipals: ["https://idp.example.com//*"] # iss/sub 형식
---
# 클레임 기반 세분화: 관리자 스코프가 있어야 쓰기 허용
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: write-requires-admin-scope
namespace: commerce
spec:
selector:
matchLabels:
app: storefront-api
action: ALLOW
rules:
- to:
- operation:
methods: ["GET"]
- to:
- operation:
methods: ["POST", "PUT", "DELETE"]
when:
- key: request.auth.claims[scope]
values: ["storefront:admin"]
운영 팁 세 가지입니다.
- jwksUri의 키는 Envoy가 주기적으로 캐시·갱신합니다. IdP에서 키를 회전할 때는 새 키를 JWKS에 먼저 추가하고 일정 기간 겹쳐 운영한 뒤 옛 키를 제거하는 표준 절차를 지키면 무중단입니다.
- audiences 검증을 생략하면 같은 IdP가 발급한 다른 서비스용 토큰이 재사용될 수 있습니다. 서비스마다 audience를 분리하고 반드시 검증하세요.
- JWT 검증은 진입 지점(ingress gateway 또는 최전방 서비스)에서 한 번 수행하고, 내부 전파는 mTLS principal과 forwardOriginalToken으로 처리하는 것이 일반적인 구성입니다.
외부 인가 — ext-authz로 OPA 연동
내장 AuthorizationPolicy는 빠르고 선언적이지만, "주문 금액이 사용자 한도 이내인가" 같은 데이터 의존 판단이나 조직 전체의 정책 코드 재사용에는 한계가 있습니다. 이럴 때 CUSTOM 액션으로 외부 인가기(OPA, 자체 인가 서비스 등)에 판단을 위임합니다.
먼저 메시 설정에 외부 인가 제공자를 등록합니다.
# MeshConfig (IstioOperator 또는 istio ConfigMap)
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
extensionProviders:
- name: opa-ext-authz
envoyExtAuthzGrpc:
service: opa.authz-system.svc.cluster.local
port: 9191
timeout: 0.5s # 인가기 지연이 전체 지연에 직결 — 짧게
그다음 CUSTOM 정책으로 적용 범위를 지정합니다.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: opa-authz
namespace: payments
spec:
selector:
matchLabels:
app: payments-api
action: CUSTOM
provider:
name: opa-ext-authz
rules:
- to:
- operation:
paths: ["/v1/charges"] # 비용이 큰 외부 인가는 필요한 경로만
OPA 쪽 정책(Rego)의 골격은 다음과 같습니다.
# Rego 정책 예시 (envoy.authz 패키지)
package envoy.authz
default allow := false
# 주문 서비스가 호출했고, JWT의 tier 클레임이 premium이면 허용
allow if {
input.attributes.source.principal ==
"spiffe://cluster.local/ns/commerce/sa/orders-api"
claims := input.attributes.metadata_context.filter_metadata["envoy.filters.http.jwt_authn"].fields
claims["tier"] == "premium"
}
설계 원칙: 외부 인가는 요청마다 네트워크 홉이 하나 추가되는 비용이 있습니다. 메시 전체에 깔지 말고, 복잡한 판단이 정말 필요한 경로에만 좁게 적용하세요. 단순한 "누가 어디를 호출할 수 있나"는 내장 ALLOW/DENY가 훨씬 저렴하고 빠릅니다. 또한 인가기 자체가 단일 장애점이 되므로, 인가기 다운 시 동작(fail-close가 기본)과 timeout을 명시적으로 결정해야 합니다.
커스텀 CA 통합 — cert-manager와 사내 PKI
기본 자체 서명 CA는 PoC에는 충분하지만, 운영 환경 특히 규제 산업에서는 다음 요구가 따라옵니다. 사내 PKI 계층에 메시 CA를 편입할 것, 루트 키를 HSM 등 안전한 곳에 보관할 것, 인증서 발급 이력을 감사할 것.
방법 1: cacerts 시크릿으로 중간 CA 주입
가장 단순한 통합입니다. 사내 루트 CA에서 메시용 중간 CA를 발급받아 istiod에 주입합니다.
# 사내 PKI에서 받은 중간 CA로 cacerts 시크릿 구성
kubectl create secret generic cacerts -n istio-system \
--from-file=ca-cert.pem \
--from-file=ca-key.pem \
--from-file=root-cert.pem \
--from-file=cert-chain.pem
# istiod 재시작 후 신규 워크로드 인증서는 이 체인으로 발급됨
이 방식의 장점은 단순함, 단점은 중간 CA 개인키가 쿠버네티스 시크릿으로 클러스터 안에 존재한다는 점입니다. etcd 암호화와 시크릿 접근 RBAC을 반드시 함께 점검해야 합니다.
방법 2: istio-csr로 cert-manager에 서명 위임
cert-manager의 istio-csr 프로젝트를 쓰면 istiod가 직접 서명하지 않고, 워크로드 CSR이 cert-manager의 Issuer로 전달됩니다. 키가 클러스터 시크릿에 머물지 않고 Vault, AWS Private CA, 사내 PKI 같은 외부 발급자로 연결할 수 있습니다.
# cert-manager Issuer 예: Vault PKI 백엔드에 서명 위임
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: vault-istio-ca
spec:
vault:
server: https://vault.corp.example:8200
path: pki_int_mesh/sign/istio-workload
auth:
kubernetes:
role: istio-csr
mountPath: /v1/auth/kubernetes
secretRef:
name: istio-csr-vault-token
key: token
istio-csr 구성 시 발급 흐름:
워크로드 istio-agent
→ CSR → istiod
→ istio-csr (gRPC CA 서버로 등록됨)
→ cert-manager CertificateRequest
→ ClusterIssuer (Vault / Private CA / 사내 PKI)
→ 서명된 인증서가 역순으로 전달
효과:
- 메시 CA 키가 클러스터 밖(HSM/Vault)에 보관됨
- 발급 이력이 cert-manager 리소스로 남아 감사 가능
- 발급자 교체가 Issuer 교체로 단순화
인증서 수명과 회전 운영
| 항목 | 기본값 | 운영 권장 |
|---|---|---|
| 워크로드 인증서 수명 | 24시간 | 24시간 유지 (짧을수록 탈취 피해 축소) |
| 갱신 시작 시점 | 수명의 약 절반 | 기본 유지 |
| 중간 CA 수명 | 10년 (자체 서명) | 1~3년 + 회전 절차 문서화 |
| 루트 CA 수명 | 10년 | PKI 정책에 따름, HSM 보관 |
워크로드 인증서는 자동 갱신되므로 운영 부담이 없지만, 중간 CA와 루트 CA의 회전은 계획이 필요합니다. 중간 CA 회전은 "새 중간 CA 주입 → istiod 재시작 → 워크로드가 24시간 내 자연 갱신되며 새 체인으로 교체"의 순서로 무중단이 가능합니다. 루트 회전은 신구 루트를 모두 신뢰하는 과도기(combined root)를 거쳐야 하므로 반드시 사전 리허설을 권합니다. 만료 감시는 다음 명령과 메트릭으로 합니다.
# 워크로드 인증서 체인과 만료 확인
istioctl proxy-config secret deploy/payments-api -n payments -o json
# istiod CA 인증서 만료 모니터링 (Prometheus)
# 메트릭: citadel_server_root_cert_expiry_timestamp
# 만료 30일 전 알림 규칙을 반드시 걸어둘 것
정책 디버깅 — 거부 로그와 dry-run
누가 거부했는지 찾기
인가 거부는 클라이언트에게 HTTP 403(또는 TCP 연결 종료)으로 나타납니다. 첫 단계는 대상 워크로드의 Envoy 로그에서 RBAC 거부 기록을 찾는 것입니다.
# 1) RBAC 디버그 로그 활성화 (대상 파드)
istioctl proxy-config log deploy/payments-api -n payments \
--level rbac:debug
# 2) 사이드카 로그에서 거부 사유 확인
kubectl logs deploy/payments-api -n payments -c istio-proxy --tail=100
# "RBAC: access denied" 와 함께 매칭 실패한 principal/경로가 출력됨
# 3) 워크로드에 적용 중인 인가 정책 전체 확인
istioctl x describe pod payments-api-6c9f7d-abcde -n payments
# 4) 정책 정합성 일괄 점검
istioctl analyze -n payments
자주 만나는 거부 원인 셋: 첫째, PERMISSIVE 상태에서 평문으로 들어와 principal이 비어 principals 조건에 매칭 실패. 둘째, 정책의 네임스페이스/서비스어카운트 오타. 셋째, ALLOW 정책을 추가하는 순간 기본 거부로 전환된다는 사실을 잊고 기존 경로를 열어두지 않은 경우입니다.
dry-run으로 무중단 정책 검증
운영 트래픽에 영향 없이 정책 효과를 미리 보려면 dry-run 어노테이션을 씁니다.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: deny-all-dryrun
namespace: payments
annotations:
istio.io/dry-run: "true" # 평가만 하고 실제로 차단하지 않음
spec: {}
dry-run 정책의 평가 결과는 Envoy 액세스 로그와 메트릭에 shadow 결과로 기록됩니다. "이 정책을 실제로 켰다면 무엇이 거부되었을까"를 운영 트래픽으로 확인한 뒤, 거부 대상이 의도와 일치할 때 어노테이션을 제거하는 것이 안전한 배포 절차입니다.
권장 정책 배포 절차:
1. dry-run으로 정책 배포
2. 일정 기간 shadow denied 로그/메트릭 수집
(istio_requests_total 및 액세스 로그의 dry-run 결과 확인)
3. 거부 목록이 의도와 일치하는지 검토
→ 의도하지 않은 거부가 있으면 rules 보완
4. dry-run 어노테이션 제거 (정책 활성화)
5. 활성화 직후 403 비율 대시보드 집중 관찰
컴플라이언스 관점 — 메시 보안이 채워주는 것
규제 환경(금융권의 전자금융감독규정에 따른 전송구간 암호화 요구, PCI DSS의 카드 데이터 전송 암호화, ISO 27001의 접근 통제 등)에서 메시 보안은 감사 대응 비용을 크게 줄여줍니다.
- 전송 암호화 증빙: STRICT mTLS가 적용된 범위에서는 "서비스 간 통신이 TLS로 암호화된다"를 정책 리소스와 메트릭(connection_security_policy)으로 증빙할 수 있습니다. 애플리케이션별 TLS 설정을 개별 확인하는 것보다 감사 범위가 단순해집니다.
- 접근 통제 증빙: AuthorizationPolicy가 곧 선언적 접근 통제 목록입니다. Git에 정책을 두면 변경 이력과 승인 절차(PR 리뷰)가 그대로 감사 추적이 됩니다.
- 키 관리: istio-csr와 외부 PKI 통합으로 "키 발급·보관·회전" 통제를 기존 PKI 감사 체계에 편입할 수 있습니다.
다만 메시 mTLS는 파드 간 구간의 암호화이므로, 클러스터 진입 전 구간(클라이언트에서 로드밸런서), 메시 밖 데이터베이스 연결, 저장 데이터 암호화는 별도 통제로 채워야 한다는 점을 감사 문서에 명확히 구분해 두시기 바랍니다. 이 글은 일반적인 기술 설명이며 특정 규제에 대한 법률 자문이 아닙니다.
성능 영향 — 측정 기준으로 말하기
mTLS와 인가의 비용은 자주 과대평가됩니다. 실측 기준으로 정리하면 다음과 같습니다.
- 핸드셰이크 비용은 커넥션 수립 시 1회입니다. Envoy 간 커넥션은 keep-alive로 재사용되므로, 정상 상태에서 요청당 비용은 대칭키 암복호화뿐이며 최신 CPU의 AES-NI에서는 매우 작습니다.
- 레이턴시 영향의 대부분은 mTLS 자체가 아니라 사이드카 프록시 경유 비용(요청당 수 밀리초 이하)입니다. 즉 "메시를 쓰기로 한 시점"에 대부분 지불한 비용이고, mTLS를 끈다고 크게 회수되지 않습니다.
- 인가 정책 평가는 Envoy 내부에서 일어나며 규칙 수십 개 수준에서는 무시할 만합니다. 다만 규칙 수백 개의 거대 정책, 과도한 regex 경로 매칭, 그리고 CUSTOM 외부 인가(네트워크 홉 추가)는 측정 후 사용해야 합니다.
- 인증서 갱신은 데이터 경로와 분리되어 있어(SDS) 트래픽에 영향을 주지 않습니다.
결론: 성능을 이유로 STRICT mTLS를 미루는 것은 대부분 근거가 약합니다. 걱정된다면 대표 서비스에서 p99 지연을 PERMISSIVE/STRICT로 비교 측정한 뒤 결정하세요.
도입 로드맵 — 관찰에서 제로트러스트까지
+------------------------------------------------------------------+
| 단계 1. 관찰 (1~2주) |
| - 사이드카 주입 완료, PERMISSIVE 기본값 유지 |
| - Kiali/Grafana로 서비스 호출 그래프와 평문 비율 파악 |
| - 산출물: 서비스 간 호출 매트릭스 (인가 정책의 설계 입력) |
+------------------------------------------------------------------+
| 단계 2. mTLS STRICT 전환 (네임스페이스 단위, 2~4주) |
| - 평문 출발지 제거 → dev부터 단계 적용 → 전역 STRICT |
| - 예외는 portLevelMtls로 좁게, 만료일 관리 |
+------------------------------------------------------------------+
| 단계 3. 인가 정책 (핵심 서비스부터) |
| - 호출 매트릭스 기반 ALLOW 정책 작성 → dry-run 검증 → 활성화 |
| - 네임스페이스 기본 거부는 충분한 dry-run 후에 |
| - 관리 경로 DENY, gateway 경유 강제 등 안전망 추가 |
+------------------------------------------------------------------+
| 단계 4. 고도화 |
| - RequestAuthentication으로 JWT 검증을 진입 지점에 |
| - 데이터 의존 판단은 OPA ext-authz로 좁게 |
| - 커스텀 CA/사내 PKI 통합, CA 만료 모니터링 체계화 |
+------------------------------------------------------------------+
각 단계의 완료 기준을 메트릭으로 정의하는 것이 중요합니다. 예를 들어 단계 2의 완료 기준은 "connection_security_policy가 none인 요청이 7일간 0건"처럼 측정 가능해야 합니다.
체크리스트
- 메시 전역 PeerAuthentication 기본값을 STRICT로 설정했다 (예외는 명시적·한시적)
- 평문 트래픽 메트릭이 0임을 확인한 뒤 STRICT로 전환했다
- 핵심 네임스페이스에 기본 거부(빈 ALLOW) 정책을 두고 필요한 경로만 열었다
- AuthorizationPolicy의 principals를 서비스어카운트(SPIFFE ID) 기준으로 작성했다
- 워크로드마다 전용 서비스어카운트를 부여했다 (default SA 공유 금지)
- JWT 검증에 issuer와 audiences를 모두 명시했다
- 새 인가 정책은 dry-run으로 검증한 뒤 활성화한다
- 403 급증과 RBAC 거부 로그에 대한 대시보드/알림이 있다
- CA 인증서 만료 알림(30일 전)을 걸어두었다
- 운영 환경은 자체 서명 CA 대신 사내 PKI/cert-manager 통합을 적용했다
- 외부 인가(CUSTOM)는 필요한 경로에만 적용하고 timeout과 장애 시 동작을 정의했다
- 인가 정책을 Git으로 관리해 변경 이력이 감사 추적으로 남는다
마치며
Istio 보안의 가치는 "제로트러스트를 코드 수정 없이, 점진적으로" 달성할 수 있다는 데 있습니다. 그 토대는 서비스어카운트 기반 SPIFFE 아이덴티티와 자동 갱신되는 단기 인증서이고, 그 위에 PeerAuthentication(암호화 강제), AuthorizationPolicy(최소 권한), RequestAuthentication(최종 사용자 검증)이 층층이 쌓입니다.
실패하는 도입의 공통점은 순서를 건너뛰는 것입니다. 평문 출발지를 정리하지 않고 STRICT를 켜거나, 호출 그래프를 모른 채 기본 거부를 깔거나, dry-run 없이 인가 정책을 운영에 밀어 넣는 경우죠. 관찰 → PERMISSIVE 정리 → STRICT → dry-run 인가 → 활성화라는 계단을 하나씩 밟으면, 메시 보안은 큰 사고 없이 도달 가능한 목표입니다. 인증서가 매일 자동으로 갱신되고, 호출 권한이 Git의 선언적 정책으로 관리되는 메시는 보안팀과 플랫폼팀 모두에게 운영 부담이 아니라 자산이 됩니다.