Published on

Kubernetes 서비스메시 mTLS 실패 원인 7가지

Authors

서버리스나 단일 애플리케이션 환경에서는 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
  • STRICT mTLS 적용 이후 일부 트래픽만 갑자기 실패
  • readiness는 정상인데 서비스 간 호출만 실패(특히 cross-namespace)

애플리케이션 레벨에서도 비슷한 증상이 보일 수 있습니다. 예를 들어 HTTP/2 기반 서비스에서 RST가 늘거나 502가 튀면 프록시 계층 문제일 가능성도 큽니다. 관련해서는 Spring Boot 3.2 HTTP/2 RST_STREAM 502 원인·해결도 함께 참고하면 진단 범위를 좁히는 데 도움이 됩니다.


원인 1) 사이드카 미주입 또는 부분 주입(한쪽만 프록시 경유)

증상

  • A에서 B로 호출 시 간헐적으로 실패하거나 특정 Pod로만 실패
  • STRICT mTLS인데 일부 워크로드는 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

해결 포인트

  • 서버(수신) 측 PeerAuthenticationSTRICT인데, 클라이언트(발신) 측이 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가 동시에 걸리면, 어느 레벨에서 드롭되는지부터 분리 진단하세요.

실전 디버깅 체크리스트(순서대로 보면 빠릅니다)

  1. 사이드카 주입 여부: 실패하는 Pod에 프록시가 있는가
  2. 정책 확인: PeerAuthentication/DestinationRule의 모드가 서로 맞는가
  3. 엔드포인트/라우팅: Service endpoints가 정상인가(0이면 mTLS가 아니라 라우팅 문제일 확률 큼)
  4. 인증서/루트 CA: 프록시 secret과 trust bundle이 동일한가
  5. 시간: not yet valid/expired가 보이면 노드 시간부터
  6. DNS/SNI: IP 직호출/하드코딩/authority 설정 확인
  7. 네트워크 레벨 차단: 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개 원인”부터 진단 플로우를 더 구체적으로 만들어 드릴게요.