- Published on
Kubernetes 서비스메시 mTLS 실패 원인 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스나 단일 애플리케이션 환경에서는 TLS가 “켜면 끝”에 가깝지만, Kubernetes 서비스메시(Istio/Linkerd 등)에서는 mTLS가 런타임(사이드카), 컨트롤플레인(인증서/정책), 네트워킹(DNS/SNI/L7), 그리고 운영(시간/롤링업데이트)까지 얽힙니다. 그래서 장애가 나면 애플리케이션 로그만 보고는 답이 안 나오고, 프록시/정책/인증서 상태를 함께 봐야 합니다.
이 글에서는 현장에서 반복적으로 등장하는 mTLS 실패 원인 7가지를 증상, 빠른 진단 명령, 해결 포인트 중심으로 정리합니다. 예시는 Istio를 기준으로 설명하지만, 원인 자체는 대부분의 서비스메시에 공통입니다.
mTLS 실패를 의심할 때 대표 증상
- 특정 서비스 호출만
503,502,upstream connect error,connection reset by peer - Envoy 로그에
TLS error,handshake failure,CERTIFICATE_VERIFY_FAILED STRICTmTLS 적용 이후 일부 트래픽만 갑자기 실패- readiness는 정상인데 서비스 간 호출만 실패(특히 cross-namespace)
애플리케이션 레벨에서도 비슷한 증상이 보일 수 있습니다. 예를 들어 HTTP/2 기반 서비스에서 RST가 늘거나 502가 튀면 프록시 계층 문제일 가능성도 큽니다. 관련해서는 Spring Boot 3.2 HTTP/2 RST_STREAM 502 원인·해결도 함께 참고하면 진단 범위를 좁히는 데 도움이 됩니다.
원인 1) 사이드카 미주입 또는 부분 주입(한쪽만 프록시 경유)
증상
- A에서 B로 호출 시 간헐적으로 실패하거나 특정 Pod로만 실패
STRICTmTLS인데 일부 워크로드는 plaintext로 접근하려고 함- 한쪽은 Envoy가 있는데 다른 한쪽은 없음
빠른 진단
kubectl -n team-a get pod -l app=a -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .spec.containers[*]}{.name}{","}{end}{"\n"}{end}'
kubectl -n team-b get pod -l app=b -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .spec.containers[*]}{.name}{","}{end}{"\n"}{end}'
- 컨테이너 목록에
istio-proxy(또는 Linkerd의linkerd-proxy)가 없는 Pod가 섞여 있으면 의심.
Istio라면 아래도 확인합니다.
kubectl get ns team-a --show-labels
# istio-injection=enabled 또는 revision 라벨 확인
해결 포인트
- 네임스페이스 라벨(
istio-injection=enabled또는istio.io/rev)을 일관되게 적용 sidecar.istio.io/inject: "false"같은 Pod 단위 예외가 있는지 점검- 롤링 재배포로 사이드카가 붙도록 보장(라벨만 바꿔도 기존 Pod는 그대로임)
원인 2) PeerAuthentication / DestinationRule 정책 불일치(STRICT vs PERMISSIVE vs DISABLE)
증상
- 특정 네임스페이스/서비스만 mTLS handshake 실패
- 정책 적용 직후부터 전면 장애 또는 부분 장애
빠른 진단
Istio 기준으로 정책 리소스를 확인합니다.
kubectl get peerauthentication -A
kubectl get destinationrule -A
Envoy 관점에서 실제로 어떤 TLS 모드로 붙는지 확인하려면:
istioctl proxy-config clusters -n team-a deploy/a | grep -n "team-b" -n
해결 포인트
- 서버(수신) 측
PeerAuthentication이STRICT인데, 클라이언트(발신) 측이DestinationRule에서ISTIO_MUTUAL을 안 쓰면 실패할 수 있습니다. - 점진 적용 시에는 서버를
PERMISSIVE로 두고, 클라이언트부터ISTIO_MUTUAL로 전환한 뒤 최종적으로STRICT로 올리는 방식이 안전합니다.
정책이 복잡해질수록 “왜 이 호출만 실패하지?”가 자주 발생합니다. 이때는 서비스 엔드포인트가 0이거나 라우팅이 꼬인 케이스도 함께 의심해야 합니다. 특히 EKS에서 엔드포인트가 비는 증상은 mTLS와 겹쳐 보일 수 있어요. EKS에서 Pod는 뜨는데 Service Endpoints가 0일 때도 같이 체크해보세요.
원인 3) 인증서 체인/루트 CA 불일치(멀티 클러스터, 루트 교체, 외부 CA 연동)
증상
- Envoy 로그에
unknown ca,unable to verify the first certificate - 특정 네임스페이스만 실패(예: 다른 revision의 control plane 사용)
- 멀티 클러스터/멀티 프라이머리에서 클러스터 간 호출만 실패
빠른 진단
Istio 사이드카가 가진 인증서/신뢰 번들을 확인합니다.
istioctl proxy-config secret -n team-a deploy/a
또는 프록시 내부 파일을 직접 확인(환경에 따라 경로는 다를 수 있음):
kubectl -n team-a exec deploy/a -c istio-proxy -- ls -al /etc/certs
해결 포인트
- 루트 CA 교체(rotate) 중에 구 루트/신 루트가 공존해야 하는데, 일부 프록시만 새 루트를 신뢰하는 상태가 되면 handshake가 깨집니다.
- 멀티 클러스터라면 동일 trust domain 및 루트 번들 동기화가 핵심입니다.
- 외부 CA를 붙였다면 intermediate 체인 누락이 흔한 함정입니다.
원인 4) 시간 동기화(NTP) 문제로 인증서 유효기간 판단 실패
증상
- 갑자기 모든 호출이 실패하거나, 특정 노드에 스케줄된 Pod만 실패
- 로그에
certificate has expired또는not yet valid
빠른 진단
노드 시간과 컨테이너 시간을 비교합니다.
kubectl get nodes -o wide
# 문제 의심 Pod에서 현재 시간 확인
kubectl -n team-a exec deploy/a -c istio-proxy -- date -u
노드 자체의 NTP 상태는 클러스터/OS별로 다르지만, 일반적으로는 systemd 기반에서:
timedatectl status
해결 포인트
- 워커 노드의 NTP/chrony 설정을 표준화
- 특정 노드풀에서만 발생한다면 AMI/이미지 차이, 부팅 후 시간 동기화 지연 등을 점검
- 인증서 TTL이 짧은 환경(예: 24h 이하)일수록 시간 오차에 더 민감합니다.
원인 5) DNS/SNI 불일치 또는 서비스 이름이 아닌 IP로 호출(인증서 SAN 매칭 실패)
증상
- 서비스 DNS로 호출하면 되는데, IP로 호출하면 실패
curl로 Pod IP 직접 호출 시만 실패- 로그에
SAN mismatch,SNI관련 경고
빠른 진단
애플리케이션이 실제로 어떤 호스트로 붙는지 확인합니다.
- 코드/설정에서
http://10.x.x.x:port같은 하드코딩이 있는지 - gRPC 클라이언트가 authority/hostname을 어떻게 세팅하는지
Istio에서는 라우팅/클러스터의 SNI 구성을 확인할 수 있습니다.
istioctl proxy-config clusters -n team-a deploy/a | grep -n "sni" -n
해결 포인트
- 서비스메시 mTLS는 보통 서비스 DNS 이름을 기준으로 SAN/SNI를 맞춥니다.
- 가능하면 Service DNS 이름으로만 호출하도록 통일하고, IP 직호출/NodePort 직호출을 피합니다.
- gRPC라면 deadline/리트라이와 함께 연결 계층 이슈가 증폭될 수 있으니, 타임아웃 전파도 같이 점검하세요. gRPC 타임아웃 지옥 탈출 - 데드라인 전파 설계에서 “타임아웃이 장애를 어떻게 증폭시키는지” 관점을 가져가면 좋습니다.
원인 6) L7 프로토콜/포트 네이밍 오류로 프록시가 프로토콜을 잘못 추론
증상
- HTTP로 보냈는데 프록시가 TCP로 취급하거나, 반대로 잘못된 L7 필터 적용
- gRPC인데 포트 네이밍이
http로 되어 있거나,tcp로 되어 있어서 라우팅/정책이 기대와 다르게 동작 - mTLS 자체보다는 “mTLS 이후의” 라우팅/업스트림 연결이 깨져 보이지만, 결과적으로 handshake/ALPN 협상이 꼬이기도 함
빠른 진단
Service 포트 이름을 확인합니다.
kubectl -n team-b get svc b -o yaml
예를 들어 Istio는 포트 이름이 http-/grpc-/tcp- 같은 규칙을 따를 때 프로토콜 추론이 안정적입니다.
해결 포인트
- Service의
spec.ports[].name을 명확히 설정 - gRPC는
grpc또는grpc-web등 의도에 맞는 네이밍 사용 - 애매한 경우(특히 1개 포트에 여러 프로토콜 혼용)에는 프로토콜 분리 또는 Gateway/VirtualService 설계를 재검토
원인 7) 네트워크 정책/보안그룹/방화벽이 프록시 제어 포트 또는 mTLS 트래픽을 차단
증상
- 같은 네임스페이스에서는 되는데, 네임스페이스 간/노드 간 통신만 실패
- 특정 AZ/서브넷/노드풀에서만 실패
i/o timeout,upstream connect error가 늘고 handshake 로그 자체가 안 남음(패킷이 도달하지 못함)
빠른 진단
- Kubernetes
NetworkPolicy적용 여부:
kubectl get networkpolicy -A
- CNI/클라우드 방화벽(EKS라면 SG/NACL) 변경 이력 확인
- 프록시가 사용하는 포트(예: envoy listener, health probe, control plane 연결)가 정책에 의해 막히지 않는지 점검
간단한 레벨에서 Pod 간 TCP 연결을 확인하려면 netshoot 같은 디버그 Pod를 씁니다.
kubectl -n team-a run netshoot --rm -it --image nicolaka/netshoot -- bash
# 내부에서
nc -vz b.team-b.svc.cluster.local 8080
해결 포인트
NetworkPolicy에서 ingress/egress 모두 필요한 포트를 열었는지 확인- 서비스메시는 “애플리케이션 포트만 열면 끝”이 아니라, 프록시 및 컨트롤플레인 통신 경로도 필요할 수 있습니다.
- 클라우드 레벨 보안그룹과 K8s NetworkPolicy가 동시에 걸리면, 어느 레벨에서 드롭되는지부터 분리 진단하세요.
실전 디버깅 체크리스트(순서대로 보면 빠릅니다)
- 사이드카 주입 여부: 실패하는 Pod에 프록시가 있는가
- 정책 확인:
PeerAuthentication/DestinationRule의 모드가 서로 맞는가 - 엔드포인트/라우팅: Service endpoints가 정상인가(0이면 mTLS가 아니라 라우팅 문제일 확률 큼)
- 인증서/루트 CA: 프록시 secret과 trust bundle이 동일한가
- 시간:
not yet valid/expired가 보이면 노드 시간부터 - DNS/SNI: IP 직호출/하드코딩/authority 설정 확인
- 네트워크 레벨 차단: NetworkPolicy/SG/NACL로 패킷이 막히는지
부록: 자주 쓰는 명령 모음(Istio 기준)
# 프록시 상태 요약
istioctl proxy-status
# 특정 워크로드의 리스너/클러스터/라우트 확인
istioctl proxy-config listeners -n team-a deploy/a
istioctl proxy-config clusters -n team-a deploy/a
istioctl proxy-config routes -n team-a deploy/a
# 프록시가 가진 인증서/비밀정보 확인
istioctl proxy-config secret -n team-a deploy/a
# xDS 설정이 꼬였는지 분석(버전에 따라 지원)
istioctl analyze -A
마무리
서비스메시 mTLS 실패는 “TLS가 깨졌다”라기보다, 주입(데이터플레인), 정책(컨트롤플레인), 이름/DNS, 운영 환경(시간/네트워크) 중 하나가 어긋난 결과인 경우가 대부분입니다. 위 7가지를 순서대로 배제해 나가면, 로그가 난해해 보여도 원인을 재현 가능한 형태로 좁힐 수 있습니다.
원하시면 사용 중인 메시(Istio인지 Linkerd인지), mTLS 모드(STRICT인지), 실패 로그 일부(Envoy access log 또는 envoy debug log), 그리고 호출 경로(같은 ns인지 cross-ns인지)를 기준으로 “가장 가능성 높은 1~2개 원인”부터 진단 플로우를 더 구체적으로 만들어 드릴게요.