Published on

EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS

Authors

서버리스나 온프레미스에서는 잘 되던 외부 HTTPS 호출이 EKS로 옮기자마자 TLS handshake timeout으로 터지는 경우가 있습니다. 이 에러는 “TLS가 느리다”가 아니라, 핸드셰이크를 시작/완료하기 전에 네트워크 경로 어딘가가 막히거나 지연된 신호인 경우가 대부분입니다. 특히 EKS에서는 다음 3축에서 자주 발생합니다.

  • IRSA(STS) 경로 문제: Pod가 AWS API를 쓰기 위해 STS(또는 OIDC/JWKS)로 나가야 하는데 egress/DNS가 깨져 토큰 교환이 지연/실패
  • VPC egress/NAT/라우팅 문제: 프라이빗 서브넷에서 인터넷으로 나가는 경로가 없거나, NAT/SG/NACL/프록시에서 드롭
  • CoreDNS/노드 DNS 구성 문제: DNS가 느리거나 NXDOMAIN/timeout이 발생해 TLS 연결 자체가 시작되지 못함

이 글은 “증상 → 원인 분리 → 최소 재현 → 해결 → 재발 방지” 순서로 정리합니다.

1) 먼저 에러를 분해하자: TLS vs DNS vs 라우팅

TLS handshake timeout은 애플리케이션 라이브러리(Go, Java, Python requests 등)가 연결/핸드셰이크 단계에서 타임아웃을 냈다는 뜻입니다. 실제 원인은 대개 아래 중 하나입니다.

  1. DNS 조회가 느리거나 실패 → 결국 IP를 못 얻어 handshake가 시작도 안 됨
  2. TCP 443 연결이 안 됨(SYN drop) → NAT/SG/NACL/라우팅/프록시
  3. SNI/인증서 체인/MTU 문제로 handshake 중간에 끊김
  4. 프록시/방화벽이 TLS를 가로채거나 특정 도메인만 차단

가장 빠른 분리 방법은 Pod 안에서 다음 3가지를 각각 확인하는 것입니다.

Pod에서 즉시 확인할 체크(원인 분리)

아래는 임시 디버그 Pod를 띄우는 예시입니다.

kubectl run -it --rm net-debug \
  --image=public.ecr.aws/docker/library/alpine:3.19 \
  --restart=Never -- sh

# alpine inside
apk add --no-cache curl bind-tools openssl ca-certificates
  1. DNS 확인
# DNS가 느리면 여기서 이미 지연/timeout이 보입니다.
time nslookup sts.amazonaws.com

time nslookup api.github.com
  1. TCP 연결(443) 확인
# TCP connect 단계 확인
curl -v --connect-timeout 5 https://sts.amazonaws.com/ 2>&1 | sed -n '1,80p'
  1. TLS handshake 자체 확인
# SNI 포함 TLS 핸드셰이크 확인
openssl s_client -connect sts.amazonaws.com:443 -servername sts.amazonaws.com -brief < /dev/null
  • nslookup이 timeout이면 CoreDNS/노드 DNS/라우팅 문제로 시작합니다.
  • nslookup은 빠른데 curl이 connect에서 멈추면 egress(NAT/SG/NACL/route) 문제입니다.
  • connect는 되는데 openssl s_client에서 멈추면 MTU/프록시/중간 장비/TLS inspection 가능성을 봅니다.

2) IRSA가 얽히면: STS 호출이 “필수 egress”가 된다

IRSA(IAM Roles for Service Accounts)를 쓰는 Pod는 AWS SDK가 내부적으로 대략 이런 흐름을 탑니다.

  1. 서비스어카운트 토큰(JWT) 읽기
  2. STS AssumeRoleWithWebIdentity 호출
  3. 임시 자격증명으로 AWS API 호출

즉, 애플리케이션이 S3/DynamoDB만 호출한다고 해도, 그 전에 STS로 반드시 나가야 합니다. 이때 STS로의 HTTPS가 TLS handshake timeout으로 막히면, 애플리케이션은 “AWS API가 느리다/죽었다”처럼 보이지만 실제론 클러스터 egress가 원인입니다.

IRSA 문제를 빠르게 확인하는 방법

Pod에 AWS CLI가 없더라도, 다음을 보면 단서가 나옵니다.

# IRSA 환경변수/웹아이덴티티 토큰 경로 확인
kubectl exec -it deploy/<your-app> -- sh -c 'env | egrep "AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_REGION"'

# 토큰 파일 존재 확인
kubectl exec -it deploy/<your-app> -- sh -c 'ls -al $AWS_WEB_IDENTITY_TOKEN_FILE && head -c 50 $AWS_WEB_IDENTITY_TOKEN_FILE'

그리고 디버그 Pod에서 STS 엔드포인트로 직접 호출해봅니다.

curl -v https://sts.amazonaws.com/ -o /dev/null
  • 여기서 handshake timeout이면 IRSA 이전에 VPC egress/DNS부터 해결해야 합니다.

STS를 PrivateLink로 쓰는 경우의 함정

보안 요구로 STS/S3 등을 VPC Endpoint(PrivateLink)로 붙였다면, 다음이 흔한 함정입니다.

  • Interface Endpoint의 SG가 443 인바운드를 허용하지 않음
  • Private DNS 미설정/충돌로 퍼블릭으로 나가려다 NAT가 없어서 실패
  • 엔드포인트가 특정 AZ에만 있고, 서브넷 라우팅/노드 배치로 지연

확인 포인트:

# VPC 엔드포인트 DNS가 내부 IP로 해석되는지
nslookup sts.amazonaws.com

# 결과가 10.x/172.16~31/192.168 대역이면 PrivateLink일 가능성이 큼

3) VPC egress: NAT/라우팅/보안그룹/NACL을 “패킷 흐름”으로 본다

프라이빗 서브넷의 노드/Pod가 인터넷으로 나가려면 일반적으로 NAT Gateway(또는 NAT 인스턴스) 가 필요합니다.

가장 흔한 시나리오

  • 노드는 프라이빗 서브넷
  • 라우트 테이블에 0.0.0.0/0 -> NAT Gateway가 없음
  • 혹은 NAT는 있는데, NAT가 있는 퍼블릭 서브넷의 라우트/IGW 구성이 잘못됨

이 경우 외부 HTTPS는 전부 실패하고, 애플리케이션은 종종 TLS handshake timeout 또는 i/o timeout으로만 보입니다.

라우팅 체크리스트(핵심만)

  • 노드가 속한 서브넷 라우트 테이블에 0.0.0.0/0이 NAT로 향하는가?
  • NAT가 있는 퍼블릭 서브넷은 0.0.0.0/0 -> IGW인가?
  • NACL이 ephemeral port(1024-65535) 왕복을 막지 않는가?

NAT 비용/트래픽 폭증까지 같이 겪고 있다면, 원인 진단과 절감은 별도 관점이 필요합니다. 이 경우는 VPC NAT Gateway 비용 폭증 10분 진단·절감도 함께 참고하면 좋습니다.

Security Group/NACL에서 놓치는 포인트

  • 노드 SG: 아웃바운드 443이 열려 있어야 함(기본 all open이 아닌 경우)
  • 엔드포인트 SG(PrivateLink): 인바운드 443을 노드 SG에서 허용해야 함
  • NACL: 인바운드/아웃바운드 모두 ephemeral 포트 범위를 허용해야 return traffic이 정상

4) CoreDNS: “DNS가 느리면 모든 게 TLS timeout처럼 보인다”

EKS에서 외부 호출 장애의 절반은 DNS에서 시작합니다. CoreDNS가 느려지면 애플리케이션 로그에는 TLS/HTTP timeout만 남고, 실제로는 도메인 해석이 안 되어 연결을 못 하는 경우가 많습니다.

CoreDNS 상태/로그 확인

kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=200

kubectl -n kube-system describe configmap coredns

로그에서 자주 보이는 신호:

  • i/o timeout (upstream resolver로 못 나감)
  • SERVFAIL 증가
  • 특정 도메인 쿼리만 지연

NodeLocal DNSCache 사용 고려

트래픽이 많거나 노드 수가 늘면 CoreDNS가 병목이 될 수 있습니다. 이때 NodeLocal DNSCache를 쓰면:

  • Pod → 노드 로컬 캐시로 질의(지연 감소)
  • CoreDNS 부하 감소
  • 네트워크 변동에 더 견고

CoreDNS 업스트림(노드의 /etc/resolv.conf) 확인

EKS는 보통 VPC DNS(예: 169.254.169.253 또는 VPC CIDR+2)를 사용합니다. 노드의 resolv.conf가 커스텀으로 바뀌었거나, 회사 DNS로 강제되어 외부 해석이 느리면 문제가 됩니다.

디버그 Pod에서 resolv.conf를 확인하세요.

cat /etc/resolv.conf
  • nameserver가 사내 DNS로 향하고 있고, 그 DNS가 VPC에서 접근 불가하면 timeout이 납니다.

5) MTU/경로 MTU 문제: connect는 되는데 handshake가 멈춘다

특히 다음 조합에서 MTU 문제가 터지기 쉽습니다.

  • VPC CNI + 특정 인스턴스 타입/ENI 설정
  • Transit Gateway, VPN, 온프레미스 연동
  • 프록시/방화벽 경유

증상 패턴:

  • DNS OK
  • TCP connect OK
  • TLS handshake에서 ClientHello/ServerHello 중간에 멈춤

간단한 힌트는 큰 패킷이 드롭되는지 보는 것입니다. 완전한 진단은 VPC Flow Logs, 노드 tcpdump가 필요하지만, 우선은 CNI/네트워크 경로 변경 직후부터 발생했는지 확인하세요.

또한 CNI 레벨에서 IP 부족/네트워크 압박이 있으면 간헐적 타임아웃으로 나타나기도 합니다. Pod가 Pending에 걸리거나 IP 할당이 불안정한 조짐이 있다면 Kubernetes CNI IP 부족으로 Pod Pending 해결 가이드도 같이 확인해두면 원인 범위를 줄일 수 있습니다.

6) 재현 가능한 “최소 테스트”로 CI처럼 검증하기

장애를 해결했는지 확신하려면, 애플리케이션이 아니라 네트워크/IRSA 경로를 직접 테스트하는 Job을 만들어두는 게 좋습니다.

예: STS + DNS + HTTPS를 한 번에 검사하는 Kubernetes Job

apiVersion: batch/v1
kind: Job
metadata:
  name: egress-tls-smoke
  namespace: default
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: smoke
          image: public.ecr.aws/docker/library/alpine:3.19
          command: ["/bin/sh", "-lc"]
          args:
            - |
              apk add --no-cache curl bind-tools openssl ca-certificates >/dev/null
              echo "== DNS ==";
              time nslookup sts.amazonaws.com;
              echo "== TCP/TLS ==";
              curl -sS -v --connect-timeout 5 https://sts.amazonaws.com/ -o /dev/null;
              echo "== OpenSSL handshake ==";
              echo | openssl s_client -connect sts.amazonaws.com:443 -servername sts.amazonaws.com -brief
  • 이 Job이 성공하면, 최소한 “DNS→443→TLS handshake” 경로는 정상입니다.
  • 실패 시 로그를 그대로 증거로 남겨 네트워크/플랫폼 팀과 합의하기 쉬워집니다.

7) 해결책을 원인별로 정리(실전 처방전)

A. IRSA/ST​S 관련

  • STS를 퍼블릭으로 쓸 경우: 프라이빗 노드면 NAT egress 필수
  • STS를 PrivateLink로 쓸 경우:
    • Interface Endpoint SG 인바운드 443을 노드 SG에서 허용
    • Private DNS 설정 확인(원치 않게 퍼블릭으로 나가려 하지 않게)
    • 엔드포인트 서브넷/AZ 분산 확인

B. VPC egress/NAT

  • 프라이빗 서브넷 라우트 테이블에 0.0.0.0/0 -> NAT 추가
  • 퍼블릭 서브넷 라우트 테이블에 0.0.0.0/0 -> IGW 확인
  • NACL에서 ephemeral port 왕복 허용
  • 회사 프록시 강제라면 HTTPS_PROXY/NO_PROXY를 Pod에 명확히 설정(특히 AWS 서비스 도메인)

C. CoreDNS

  • CoreDNS Pod 리소스 상향(CPU throttling이면 DNS 지연이 급증)
  • NodeLocal DNSCache 도입
  • CoreDNS ConfigMap에서 불필요한 forward 체인/외부 DNS를 제거하고 VPC DNS로 단순화

8) 운영 팁: 관측을 붙이면 다음 장애가 빨라진다

  • 애플리케이션에선 dns lookup time, tcp connect time, tls handshake time을 분리해 로깅/메트릭화(예: Envoy, OpenTelemetry)
  • CoreDNS metrics(coredns_dns_request_duration_seconds)를 Prometheus로 수집
  • VPC Flow Logs로 443 드롭/REJECT 확인

EKS의 TLS handshake timeout은 결론적으로 TLS 자체 문제인 경우가 드물고, 대부분은 IRSA(STS)·VPC egress·CoreDNS 중 하나에서 “외부로 나가는 기본 경로”가 깨진 결과입니다. 위 순서대로 DNS → 443 connect → TLS handshake → IRSA(STS) 호출을 분리해 확인하면, 감으로 고치지 않고도 30분 내에 원인 축을 특정할 수 있습니다.