Published on

EKS Pod간 TLS 실패? 인증서·SNI·mTLS 10분 진단

Authors

서로 잘 통신하던 Pod 간 호출이 갑자기 x509: certificate signed by unknown authority, tls: handshake failure, handshake alert: unrecognized_name, EOF 같은 TLS 에러로 터지면 대부분은 인증서/이름(SNI)/클라이언트 인증(mTLS)/시간/경로 중 하나가 어긋난 겁니다. 문제는 로그 한 줄만 보고는 원인을 단정하기 어렵다는 점이죠.

이 글은 EKS(쿠버네티스) 환경에서 Pod-to-Pod TLS 실패를 10분 안에 분류하고, 재현/검증/수정까지 빠르게 이어지도록 현장에서 바로 쓰는 커맨드 중심으로 정리합니다.

> 참고: TLS가 갑자기 전반적으로 실패하고 STS까지 같이 흔들린다면 시간 문제일 가능성이 큽니다. 아래 글도 함께 확인하세요.
> - EKS Pod 시간 드리프트로 STS·TLS 실패 해결하기

0) 10분 진단 로드맵(결론부터)

아래 순서대로 보면 “원인 범주”가 거의 확정됩니다.

  1. 서버 인증서가 무엇을 내주는지 확인(체인/만료/SubjectAltName)
  2. SNI가 올바른지 확인(서비스 DNS로 접속하는지, IP로 찍고 있지 않은지)
  3. mTLS 여부 확인(서버가 클라이언트 인증서를 요구하는지)
  4. 시간 드리프트 확인(특히 not yet valid, expired류)
  5. 경로/프록시/사이드카 확인(Envoy/Istio/NGINX에서 TLS 종료 위치)
  6. 네트워크는 마지막(대부분은 TLS 이전 단계에서 이미 힌트가 나옴)

이제 각 단계를 커맨드로 쪼개서 보겠습니다.

1) 증상 문자열로 1차 분류

TLS 실패 메시지는 꽤 정직합니다. 아래 표처럼 먼저 분류해두면 불필요한 삽질이 줄어듭니다.

  • x509: certificate signed by unknown authority
    • CA 번들 누락, 잘못된 체인(중간 인증서 누락), 사설 CA인데 trust store에 없음
  • x509: certificate is valid for ..., not <host>
    • SAN 불일치(인증서에 들어있는 DNS 이름과 접속 호스트가 다름)
  • remote error: tls: bad certificate
    • (mTLS에서 흔함) 클라이언트 인증서가 잘못됨 또는 서버가 신뢰하지 않음
  • handshake alert: unrecognized_name
    • SNI 불일치(서버가 SNI 기반으로 cert 선택, 클라이언트가 다른 이름으로 요청)
  • certificate has expired / not yet valid
    • 시간 드리프트 또는 cert 갱신 실패
  • EOF, connection reset by peer
    • L7 프록시/사이드카가 중간에서 끊음, 포트 프로토콜 불일치(HTTP로 TLS 포트 접근 등)

2) “서버가 실제로 내주는 인증서”부터 뜯어보기

클라이언트 컨테이너의 trust store가 어떻든, 먼저 서버가 무엇을 내주는지를 봐야 합니다.

2-1) Pod 내부에서 openssl로 서버 인증서/체인 확인

아래는 가장 강력한 1줄입니다. 핵심은 -servername(SNI)입니다.

# 디버그용 임시 Pod에서 실행 권장
# 대상: my-svc.my-ns.svc.cluster.local:443
openssl s_client \
  -connect my-svc.my-ns.svc.cluster.local:443 \
  -servername my-svc.my-ns.svc.cluster.local \
  -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

여기서 확인할 것:

  • notBefore/notAfter: 만료/미래 유효 여부
  • subjectAltName: DNS 이름이 들어있는지
  • issuer: 사설 CA인지, 공인 CA인지

체인까지 보고 싶다면:

openssl s_client \
  -connect my-svc.my-ns.svc.cluster.local:443 \
  -servername my-svc.my-ns.svc.cluster.local \
  -showcerts </dev/null

출력에 중간 인증서가 누락되어 있으면(서버가 leaf만 주는 경우) 클라이언트에서 unknown authority가 나기 쉽습니다.

2-2) curl로 “검증 실패 이유”를 더 읽기

curl -vk https://my-svc.my-ns.svc.cluster.local/healthz
  • * SSL certificate problem: unable to get local issuer certificate → 중간/루트 CA 문제
  • * subjectAltName does not match → SAN 불일치

3) SNI 문제: IP로 때리면 거의 망합니다

EKS 내부 통신에서 흔한 실수는 Service의 ClusterIP로 직접 호출하거나, 환경변수/설정에 IP가 박혀서 호출되는 케이스입니다. TLS는 기본적으로 이름 기반 검증이고, 서버도 SNI로 인증서를 고릅니다.

3-1) SNI 미지정/불일치 재현

아래처럼 -servername 없이 붙으면(또는 다른 이름을 주면) 서버가 기본 인증서를 내줄 수 있습니다.

# SNI 없이 접속(문제 재현용)
openssl s_client -connect my-svc.my-ns.svc.cluster.local:443 </dev/null

# 틀린 SNI로 접속
openssl s_client \
  -connect my-svc.my-ns.svc.cluster.local:443 \
  -servername wrong.host.example \
  </dev/null

unrecognized_name 또는 전혀 다른 인증서가 나오면 SNI 라우팅/가상호스트 구성이 있는 것입니다(Envoy/Ingress/NGINX 등).

3-2) 해결 체크

  • 애플리케이션이 DNS 이름으로 호출하도록 강제
  • gRPC라면 :authority/SNI가 무엇으로 잡히는지 확인
  • Istio/Envoy를 쓰면 DestinationRule/ServiceEntry/Gateway의 TLS 모드 확인

4) mTLS 문제: 서버가 클라이언트 인증서를 요구하는지 확인

서버가 mTLS를 요구하면, 클라이언트가 인증서를 안 주는 순간 바로 실패합니다. openssl s_client로 확인 가능합니다.

openssl s_client \
  -connect my-svc.my-ns.svc.cluster.local:443 \
  -servername my-svc.my-ns.svc.cluster.local \
  -state -tlsextdebug </dev/null

출력 중에 아래 같은 힌트가 보이면 mTLS 의심:

  • CertificateRequest가 온다(서버가 클라이언트 인증서 요구)

4-1) 클라이언트 인증서로 붙어보기

# client.crt/client.key, 그리고 서버가 신뢰하는 client CA가 맞아야 함
openssl s_client \
  -connect my-svc.my-ns.svc.cluster.local:443 \
  -servername my-svc.my-ns.svc.cluster.local \
  -cert /etc/mtls/client.crt \
  -key /etc/mtls/client.key \
  -CAfile /etc/mtls/ca.crt \
  </dev/null

여기서 성공하면 “네트워크”가 아니라 “클라이언트 인증서 배포/로테이션/권한” 문제로 범위가 확 줄어듭니다.

4-2) 흔한 원인

  • Secret이 갱신됐는데 Pod가 재시작되지 않아 구 인증서를 들고 있음
  • ca.crt가 바뀌었는데 클라이언트 trust store가 그대로
  • cert-manager 로테이션 타이밍에 앱이 reload를 못 해서 간헐 실패

5) 시간 드리프트: not yet valid는 거의 확정 신호

클러스터 노드 시간은 정상인데 특정 Pod에서만 TLS가 실패하는 경우도 있습니다(컨테이너 이미지의 tz/시간 설정, 노드 문제, 일시적 드리프트 등). 인증서 검증은 시간에 민감합니다.

5-1) Pod에서 시간 확인

# 대상 Pod 내부
date -u

# 노드 시간도 비교하고 싶다면(권한이 있다면)
kubectl get node -o wide

증상이 STS/외부 HTTPS까지 같이 흔들린다면 시간 문제일 확률이 올라갑니다. 자세한 해결 흐름은 아래 글을 참고하세요.

6) “어디서 TLS가 종료되는가”를 먼저 확정하기

Pod-to-Pod TLS라고 해도 실제로는 다음 중 하나일 수 있습니다.

  • 앱 ↔ 앱이 직접 TLS
  • 앱 ↔ 사이드카(Envoy)에서 TLS 종료 후 평문으로 전달
  • Ingress/Service Mesh Gateway에서 TLS 종료

종료 지점이 다르면 봐야 할 설정 파일이 달라집니다.

6-1) 서비스 포트/타겟포트 확인

kubectl -n my-ns get svc my-svc -o yaml
kubectl -n my-ns get endpointslices -l kubernetes.io/service-name=my-svc -o yaml
  • port: 443인데 targetPort: 8080이면, 서비스는 443으로 받지만 백엔드는 8080(평문)일 수 있습니다.
  • 클라이언트가 TLS로 8080을 때리면 EOF/reset가 날 수 있습니다.

6-2) Pod에 사이드카가 있는지 확인

kubectl -n my-ns get pod my-pod -o jsonpath='{.spec.containers[*].name}'

istio-proxy, envoy 같은 컨테이너가 보이면, TLS 정책은 앱보다 메시 설정이 우선일 수 있습니다.

7) DNS/네트워크는 “TLS 단서 확보 후”에 확인

TLS는 L4 연결이 된 다음에야 시작됩니다. 따라서 먼저 connect가 되는지 확인하고, 그 다음 TLS를 봐야 합니다.

7-1) 연결성 체크

# TCP 레벨
nc -vz my-svc.my-ns.svc.cluster.local 443

# DNS
getent hosts my-svc.my-ns.svc.cluster.local

연결이 불안정하거나 특정 노드/서브넷에서만 실패하면 egress/NAT/SNAT 경로도 의심해야 합니다. 특히 외부로 나가는 TLS가 간헐 실패라면 아래 글이 도움이 됩니다.

8) 실전: 디버그용 ephemeral Pod 템플릿

EKS에서 문제를 빨리 좁히려면 “도구가 들어있는” 디버그 Pod를 하나 띄우는 게 가장 빠릅니다.

kubectl run -n my-ns tls-debug --rm -it \
  --image=nicolaka/netshoot \
  -- bash

# 들어가서
# openssl, curl, dig, tcpdump 등을 사용

Istio 환경이라면 sidecar 주입을 끄고(또는 켜고) 비교하는 것도 유용합니다.

kubectl run -n my-ns tls-debug-no-sidecar --rm -it \
  --image=nicolaka/netshoot \
  --overrides='{"metadata":{"annotations":{"sidecar.istio.io/inject":"false"}}}' \
  -- bash

9) 자주 나오는 원인별 “바로 고치는” 체크리스트

9-1) unknown authority

  • 서버가 fullchain을 제공하는지 확인(중간 인증서 포함)
  • 클라이언트 컨테이너에 사설 CA를 넣었는지 확인
    • Debian/Ubuntu: /usr/local/share/ca-certificates/*.crt + update-ca-certificates
    • Alpine: /usr/local/share/ca-certificates + update-ca-certificates

9-2) SAN 불일치

  • 인증서를 my-svc.my-ns.svc.cluster.local(또는 실제 호출 호스트)로 재발급
  • 호출 호스트를 인증서 SAN에 맞게 변경

9-3) SNI 문제

  • IP 호출 금지, DNS 호출 강제
  • gRPC/HTTP 클라이언트에서 SNI/Host 헤더를 명시

9-4) mTLS 실패

  • 서버가 요구하는 client cert 체인 확인
  • Secret 로테이션 시 앱 reload/Pod restart 전략 점검
  • 메시 정책(PeerAuthentication/DestinationRule 등)과 앱 설정 충돌 여부 확인

9-5) 시간

  • 노드 NTP/chrony 상태 점검
  • 특정 노드에서만 발생하면 노드 교체/드레인 고려

10) 마무리: “openssl 한 번”으로 절반은 끝납니다

Pod-to-Pod TLS 장애는 복잡해 보이지만, 실제로는 다음 3가지만 빨리 확인하면 절반 이상이 정리됩니다.

  1. 서버가 내주는 인증서(SAN/체인/만료)
  2. SNI가 맞는지(이름으로 접속하는지)
  3. mTLS로 클라이언트 인증서를 요구하는지

그 다음에야 시간/프록시/네트워크를 보세요. 특히 -servername을 넣은 openssl s_client는 “지금 이 경로에서 실제로 어떤 인증서가 오가는지”를 가장 빠르게 보여주는 도구입니다.

문제 상황의 에러 로그(클라이언트/서버), 대상 서비스 구성(svc/ingress/gateway), 그리고 위 openssl s_client -showcerts 출력 일부를 함께 보면 원인 좁히는 속도가 훨씬 빨라집니다.