- Published on
EKS TLS handshake timeout 원인·해결 9가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 살아 있는데도 클라이언트가 TLS handshake timeout으로 실패하면, 문제는 “애플리케이션 로직”보다 **핸드셰이크가 끝나기 전에 끊기는 경로(네트워크/프록시/리소스/인증서/이름해석)**에 있는 경우가 많습니다. 특히 EKS에서는 Pod ↔ Service ↔ (kube-proxy/IPVS/iptables) ↔ Node ↔ (CNI) ↔ (NLB/ALB) ↔ 클라이언트로 이어지는 경로가 길어, 어디서든 지연/드롭이 생기면 핸드셰이크가 타임아웃으로 보입니다.
이 글은 “원인 후보를 많이 나열”하는 수준이 아니라, 각 원인을 어떻게 확인하고, 어떤 설정/조치로 해결하며, 어떤 방식으로 재발을 막는지를 9가지로 정리합니다.
먼저: 증상 구분(클라이언트/서버/중간장비)
같은 메시지라도 발생 지점에 따라 의미가 다릅니다.
- 클라이언트 로그:
TLS handshake timeout,context deadline exceeded(Go),read: connection reset by peer등 - 서버(인그레스/Envoy/Nginx) 로그:
client prematurely closed connection,SSL_do_handshake() failed - 로드밸런서/네트워크: SYN 재전송, TLS ClientHello가 도착하지 않음, 혹은 ServerHello가 돌아오지 않음
5분 내 1차 재현/확인 커맨드
아래는 “어느 구간에서 느려졌는지”를 빠르게 가늠하는 최소 도구입니다.
# 1) DNS 확인
kubectl -n <ns> exec -it <pod> -- nslookup <host>
# 2) TCP 연결/핸드셰이크 시간 측정 (클라이언트 관점)
# -servername: SNI 지정(인그레스/ALB에서 중요)
# -tls1_2/-tls1_3로 프로토콜 강제도 가능
kubectl -n <ns> exec -it <pod> -- sh -lc \
'time openssl s_client -connect <host>:443 -servername <host> -brief < /dev/null'
# 3) HTTP 레벨까지 포함해 전체 타이밍
kubectl -n <ns> exec -it <pod> -- sh -lc \
'curl -vk --connect-timeout 3 --max-time 10 https://<host>/healthz'
핸드셰이크 단계에서 멈추면 openssl s_client가 오래 걸리거나 타임아웃으로 끝납니다. 반면 핸드셰이크는 끝나는데 응답이 느리면 HTTP 레벨(업스트림/앱) 문제일 가능성이 큽니다.
원인 1) CoreDNS/노드 DNS 지연 또는 NXDOMAIN 플랩
TLS 핸드셰이크 전에 대부분 DNS 조회가 먼저 일어납니다. DNS가 느리면 클라이언트는 “연결 자체가 느리다”로 느끼고, 라이브러리에 따라 TLS handshake timeout으로 뭉뚱그려 표출되기도 합니다.
확인 방법
# CoreDNS 상태
kubectl -n kube-system get deploy coredns
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=200
# Pod에서 DNS 응답 시간 측정
kubectl -n <ns> exec -it <pod> -- sh -lc \
'for i in $(seq 1 10); do \
/usr/bin/time -f "%e" nslookup <host> >/dev/null; \
done'
해결 체크리스트
- CoreDNS 리소스(CPU) 부족 시 스케일 아웃/리소스 상향
stubDomains,ndots,timeout설정 검토- VPC DNS(특히 온프렘 포워딩) 병목이면 경로 단순화
IPv6/듀얼스택 환경에서 DNS AAAA 레코드 처리/라우팅이 꼬이면 “연결은 시도하지만 돌아오지 않는” 형태로 보일 수 있습니다. 듀얼스택/IPv6 관련 점검은 아래 글의 체크리스트가 그대로 도움이 됩니다.
원인 2) 보안그룹/네트워크폴리시로 443 경로가 부분 차단(비대칭)
curl은 되는데 특정 라이브러리/특정 Pod만 실패하는 경우, SG/NACL/NetworkPolicy가 “완전 차단”이 아니라 특정 노드/서브넷/포트 범위에서만 드롭하는 케이스가 흔합니다. TLS는 왕복이 필요하므로, 비대칭 라우팅/부분 드롭이면 핸드셰이크가 쉽게 타임아웃됩니다.
확인 방법
- 같은 Pod에서 대상 IP를 직접 찍어서 테스트
- 노드 서브넷별로 재현되는지 확인
- VPC Flow Logs에서
REJECT/NODATA확인
# 대상 호스트의 IP를 얻고, IP로 직접 연결 시도
kubectl -n <ns> exec -it <pod> -- sh -lc \
'getent hosts <host> && openssl s_client -connect <ip>:443 -servername <host> -brief < /dev/null'
해결 체크리스트
- 노드 SG ↔ LB SG ↔ 백엔드 SG 인바운드/아웃바운드 443, ephemeral port 확인
- NACL이 ephemeral port를 막지 않는지 확인
- NetworkPolicy가 egress 443/DNS를 허용하는지 확인
원인 3) NAT 게이트웨이/인터넷 egress 병목(특히 STS/외부 API)
EKS 워커가 프라이빗 서브넷이고 외부로 나갈 때 NAT를 타면, NAT 포트 고갈/대역폭/라우팅 문제로 외부 TLS 핸드셰이크가 타임아웃될 수 있습니다. STS, 외부 결제/인증 API, SaaS 호출이 갑자기 handshake timeout으로 터질 때 의심해야 합니다.
확인 방법
- 실패 대상이 “외부 도메인”인지
- 같은 VPC 내 엔드포인트로는 정상인지
- NAT GW 지표(ActiveConnectionCount, ErrorPortAllocation 등) 확인
해결 체크리스트
- 가능하면 VPC Interface Endpoint(PrivateLink)로 우회(STS, ECR, CloudWatch 등)
- NAT를 AZ별로 분산/스케일
- 대량 egress 워크로드는 프록시/커넥션 풀 적용
STS 계열 타임아웃은 증상이 유사하고 원인도 NAT/DNS/VPC 설정이 많습니다. 아래 글의 진단 순서를 같이 보면 빠릅니다.
원인 4) ALB/NLB 리스너·타겟 그룹 헬스/프로토콜 불일치
인그레스 앞단(ALB) 또는 서비스 타입 LoadBalancer(NLB)에서 TLS 종료/패스스루 구성이 꼬이면, 클라이언트는 핸드셰이크 단계에서 멈춥니다.
대표적인 실수:
- ALB는 HTTPS 리스너인데 백엔드는 HTTP로 기대(또는 반대)
- NLB TLS 리스너에서 SNI/인증서 설정 누락
- 타겟 그룹 헬스체크가 실패해 트래픽이 죽은 타겟으로 분산
확인 방법
- AWS 콘솔에서 리스너 프로토콜/포트, 타겟 그룹 프로토콜/헬스 확인
- 인그레스 컨트롤러 로그에서 업스트림 연결 실패 확인
# Ingress 리소스 이벤트로 힌트 찾기
kubectl describe ingress -n <ns> <ingress>
# AWS Load Balancer Controller 로그 확인
kubectl -n kube-system logs deploy/aws-load-balancer-controller --tail=200
ALB 계열 타임아웃/지연 문제는 TLS 자체가 아니라 “요청이 LB에서 오래 머무는” 형태로도 나타납니다. 408/타임아웃 튜닝은 아래 글도 참고하세요.
원인 5) 인증서 체인/키 타입/암호군 불일치(클라이언트 호환성)
TLS 핸드셰이크 타임아웃처럼 보이지만, 실제로는 협상 실패가 느리게/재시도로 나타나 타임아웃으로 끝나는 경우가 있습니다.
- 중간 인증서 누락(서버가 fullchain을 안 줌)
- RSA/ECDSA 선택 문제(특정 클라이언트만 실패)
- TLS 1.3만 허용/레거시 클라이언트 불가
- 너무 큰 인증서 체인/OCSP 문제
확인 방법
# 서버가 제공하는 체인 확인
openssl s_client -connect <host>:443 -servername <host> -showcerts < /dev/null
# 지원 프로토콜/암호군 대략 확인
# (정밀 스캔은 testssl.sh 같은 도구 권장)
openssl s_client -connect <host>:443 -servername <host> -tls1_2 < /dev/null
openssl s_client -connect <host>:443 -servername <host> -tls1_3 < /dev/null
해결 체크리스트
- Ingress/Nginx/Envoy에서 fullchain 제공
- ACM 사용 시 올바른 인증서가 리스너에 연결됐는지 확인
- 클라이언트군(모바일/레거시 JVM 등)에 맞춰 TLS 정책 조정
원인 6) SNI/Host 헤더 불일치로 잘못된 인증서/가상호스트로 라우팅
ALB/Ingress가 SNI 기반으로 인증서를 선택하거나, Nginx가 server_name으로 라우팅할 때 SNI 누락 또는 Host 불일치가 있으면 엉뚱한 인증서가 나가거나 디폴트 백엔드로 떨어져 핸드셰이크가 지연/실패할 수 있습니다.
확인 방법
# SNI를 명시했을 때/안 했을 때 결과 비교
openssl s_client -connect <lb-dns>:443 -brief < /dev/null
openssl s_client -connect <lb-dns>:443 -servername <expected-host> -brief < /dev/null
# curl로 Host 강제
curl -vk https://<lb-dns>/ -H 'Host: <expected-host>'
해결 체크리스트
- 클라이언트가 SNI를 보내는지(특히 오래된 라이브러리)
- Ingress
tls.hosts와 실제 도메인 일치 - ALB 인증서 SNI 매핑 확인
원인 7) 노드/Pod 리소스 압박(CPU throttling, conntrack, 파일디스크립터)
TLS 핸드셰이크는 CPU를 꽤 씁니다(키 교환/서명 검증). 노드나 Pod가 CPU throttling을 심하게 당하거나, conntrack 테이블이 꽉 차거나, FD가 부족하면 새 연결이 느려져 핸드셰이크가 타임아웃으로 보일 수 있습니다.
확인 방법
# Pod/노드 리소스 확인
kubectl top pod -n <ns>
kubectl top node
# 이벤트에서 OOM/리소스 압박 힌트
kubectl get events -A --sort-by=.lastTimestamp | tail -n 50
# 노드 conntrack 사용량(노드에 접근 가능할 때)
# sudo conntrack -S
# cat /proc/sys/net/netfilter/nf_conntrack_count
# cat /proc/sys/net/netfilter/nf_conntrack_max
해결 체크리스트
- TLS 종료(Envoy/Nginx) Pod에 CPU limit이 너무 낮지 않은지(특히 limit=100m 같은 값)
- HPA/Cluster Autoscaler로 스파이크 흡수
- conntrack max 상향 또는 트래픽 분산
리소스 압박이 동반되면 OOMKilled/GC 스톨 등으로 더 악화됩니다. 메모리/리소스 진단은 아래 글도 같이 보면 좋습니다.
원인 8) MTU/PMTUD 문제로 TLS 패킷 단편화가 드롭
EKS에서 CNI, ENI, 터널링, 온프렘 VPN/Transit Gateway 등 네트워크 경로가 복잡해지면 MTU 불일치로 큰 패킷이 중간에서 드롭되고, 특히 TLS 핸드셰이크의 인증서 전송 구간에서 멈추는 일이 있습니다.
특징:
- 작은 요청은 되는데, 특정 인증서 체인/특정 경로에서만 실패
curl은 간헐적 성공, 대량 동시성에서 실패 증가
확인 방법
# DF 비트로 MTU 추정(대상 네트워크에 따라 ICMP가 막혀있으면 한계가 있음)
# 1472(payload)+28(IP/ICMP)=1500
kubectl -n <ns> exec -it <pod> -- sh -lc \
'ping -M do -s 1472 <target-ip> -c 3 || true'
# 더 작은 사이즈로 내려가며 성공하는 지점 확인
kubectl -n <ns> exec -it <pod> -- sh -lc \
'for s in 1472 1464 1400 1300 1200; do echo "size=$s"; ping -M do -s $s <target-ip> -c 1; done'
해결 체크리스트
- VPC CNI/노드 네트워크 MTU를 경로에 맞게 조정
- TGW/VPN/온프렘 장비 MTU/클램핑 확인
- 가능하면 ICMP(Frag needed) 허용하여 PMTUD 정상화
원인 9) 클라이언트 라이브러리 타임아웃/커넥션 풀 설정 문제(Go/Java/Envoy)
서버는 정상인데 특정 마이크로서비스만 TLS handshake timeout이 난다면, 클라이언트의 타임아웃이 너무 짧거나 커넥션 풀이 고갈된 경우가 많습니다.
- Go
http.Transport의TLSHandshakeTimeout기본 10s(환경에 따라 짧을 수 있음) - Keep-Alive 비활성화로 매 요청마다 핸드셰이크 수행
- 프록시/사이드카(Envoy)에서 동시 연결 제한
확인 방법
- 실패 시점에 클라이언트의 goroutine/thread dump, 커넥션 풀 메트릭 확인
- 재시도 폭증 여부 확인(재시도가 더 큰 부하를 만들어 악순환)
Go 예시: 타임아웃/풀 튜닝
package main
import (
"crypto/tls"
"net"
"net/http"
"time"
)
func newClient() *http.Client {
dialer := &net.Dialer{
Timeout: 3 * time.Second,
KeepAlive: 30 * time.Second,
}
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
MaxIdleConns: 200,
MaxIdleConnsPerHost: 50,
IdleConnTimeout: 90 * time.Second,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
return &http.Client{
Transport: tr,
Timeout: 15 * time.Second, // 전체 요청 타임아웃
}
}
func main() {
_ = newClient()
}
핵심은 “핸드셰이크 타임아웃만 늘리기”가 아니라, 연결 재사용(Keep-Alive), DNS/다이얼/헤더 타임아웃을 분리해서 병목 구간을 드러내는 것입니다.
실전 디버깅 루틴(재발 방지용)
1) 경로를 3단으로 쪼개서 측정
- Pod → Service(ClusterIP)
- Pod → Ingress/LB 내부 주소
- Pod → 외부 도메인(공인)
각 단계에서 openssl s_client 시간을 비교하면 “어디부터 느려지는지”가 보입니다.
2) 관측 지표를 최소 4종은 고정
- CoreDNS latency/오류율
- 노드 CPU throttling/로드
- conntrack 사용량
- LB 타겟 헬스 및 4xx/5xx/TargetResponseTime
3) 장애 시 즉시 남길 증거(템플릿)
# 장애 시각에 아래를 한 번에 수집
kubectl -n <ns> get pod -o wide
kubectl -n <ns> describe pod <pod>
kubectl -n kube-system logs deploy/aws-load-balancer-controller --since=10m
kubectl -n kube-system logs -l k8s-app=kube-dns --since=10m
마무리
EKS에서 TLS handshake timeout은 단일 원인이라기보다, DNS 지연, 네트워크 드롭, LB 프로토콜 불일치, MTU 문제, 리소스 압박, 클라이언트 풀 고갈 같은 요소가 겹치며 나타나는 “현상명”에 가깝습니다.
가장 빠른 접근은 (1) openssl s_client로 핸드셰이크 지점에서 멈추는지 확인하고, (2) DNS/네트워크/LB/리소스를 위 9가지 체크리스트로 좁혀가며, (3) 재발 방지를 위해 지표와 타임아웃을 분리해 관측 가능하게 만드는 것입니다.