Published on

EKS에서 TLS 1.3만 실패할 때 - OpenSSL·ALPN

Authors

서론

EKS에서 서비스 통신이 “어떤 클라이언트에서는 되는데”, 혹은 “TLS 1.2로는 되는데 TLS 1.3만 실패”하는 문제는 생각보다 자주 만납니다. 특히 Ingress/Load Balancer(예: ALB/NLB) 앞단, 서비스 메시(Envoy/Istio), gRPC/HTTP2, 그리고 언어 런타임(OpenSSL/boringssl) 조합이 섞이면 증상이 더 난해해집니다.

이 글은 다음 상황을 전제로 합니다.

  • 동일한 엔드포인트에 대해 TLS 1.2는 성공
  • TLS 1.3handshake failure, unexpected eof, no application protocol 같은 오류로 실패
  • 실패가 특정 클라이언트(OpenSSL 버전, curl, Java, gRPC)에서만 발생하거나, 특정 경로(ALB/Envoy/Nginx 경유)에서만 발생

핵심은 TLS 1.3 자체의 암호군 문제라기보다, 실제 현장에서는 ALPN(HTTP/2 협상), SNI/리스너/인증서 체인, 프록시의 프로토콜 다운그레이드, 중간 장비의 TLS 1.3 지원/설정 불일치가 원인이 되는 경우가 많다는 점입니다.

관련해서 Ingress/ALB/NLB 경로에서 5xx가 섞여 나오면 먼저 로드밸런서 헬스체크부터 확인하는 것도 좋습니다. (증상이 502/504로 보일 때 참고) AWS ALB 502·504 난사 - 원인별 해결 체크리스트


TLS 1.3만 실패할 때의 전형적인 패턴

1) curl은 되는데 grpcurl/gRPC만 실패

  • TLS는 성공하는데 gRPC가 UNAVAILABLE 또는 transport: authentication handshake failed로 실패
  • 원인은 대개 ALPN에서 h2 협상이 안 됨

2) OpenSSL로 -tls1_2는 성공, -tls1_3는 실패

  • openssl s_client -tls1_3에서 no application protocol 또는 alert handshake failure
  • 서버(또는 중간 프록시)가 TLS 1.3에서만 ALPN/프로토콜 정책이 달라지는 경우

3) 브라우저는 되는데 서버-서버 호출만 실패

  • 브라우저는 HTTP/1.1로도 잘 폴백하지만, 특정 SDK/에이전트는 HTTP/2를 강제하거나 ALPN 요구가 엄격

ALPN이 무엇이고 왜 TLS 1.3에서 더 자주 문제를 드러내나

ALPN(Application-Layer Protocol Negotiation)은 TLS 핸드셰이크 단계에서 “이 연결 위에 HTTP/1.1을 올릴지, HTTP/2(h2)를 올릴지” 등을 협상하는 확장입니다.

  • HTTP/2는 사실상 ALPN이 필수입니다.
  • gRPC는 대부분 h2가 필수입니다.

TLS 1.2에서도 ALPN을 쓰지만, 환경에 따라 TLS 1.2 경로에서는 우연히 HTTP/1.1로 폴백되어 “되는 것처럼” 보이다가, TLS 1.3 경로에서 클라이언트가 h2를 강하게 요구하거나 서버/프록시가 ALPN 응답을 제대로 못해 실패가 표면화되는 경우가 많습니다.


1단계: OpenSSL로 증상 재현과 1차 분류

가장 먼저 클라이언트가 무엇을 제안했고 서버가 무엇을 선택했는지를 확인합니다.

OpenSSL로 TLS 1.2/1.3 비교

# TLS 1.2로 강제
openssl s_client -connect example.com:443 -servername example.com -tls1_2 -alpn "h2,http/1.1" -brief

# TLS 1.3로 강제
openssl s_client -connect example.com:443 -servername example.com -tls1_3 -alpn "h2,http/1.1" -brief

여기서 봐야 할 포인트:

  • ALPN protocol: h2 또는 ALPN protocol: http/1.1가 찍히는지
  • TLS 1.3에서만 no application protocol이 뜨는지
  • Verify return code가 0인지(인증서 체인 문제 여부)

ALPN을 일부러 빼서 확인

# ALPN 미제공
openssl s_client -connect example.com:443 -servername example.com -tls1_3 -brief
  • ALPN을 제공했을 때만 실패한다면, 거의 확실히 ALPN 처리(서버/프록시/로드밸런서)가 문제입니다.

curl로 HTTP/2 강제

# HTTP/2 강제
curl -v --http2 https://example.com/

# HTTP/1.1 강제
curl -v --http1.1 https://example.com/
  • --http1.1은 되는데 --http2만 실패하면, TLS 자체보다는 h2 경로(=ALPN/프록시 설정) 문제일 확률이 큽니다.

2단계: EKS 트래픽 경로를 “TLS 종료 지점” 기준으로 쪼개기

TLS 1.3 실패를 풀려면, 먼저 “어디에서 TLS가 종료(terminate)되는지”를 명확히 해야 합니다.

A. ALB에서 TLS 종료(HTTPS Listener) → 타겟은 HTTP

  • 클라이언트↔ALB 구간만 TLS
  • 파드/서비스는 HTTP
  • 이 경우 TLS 1.3/ALPN은 ALB 설정 영향을 크게 받습니다.

B. NLB TCP 패스스루 → 파드/Ingress가 TLS 종료

  • 클라이언트↔파드(또는 Ingress)까지 TLS가 그대로 전달
  • 이 경우 파드의 Envoy/Nginx/애플리케이션(OpenSSL)이 직접 TLS 1.3/ALPN을 처리

C. 중간에 Envoy/Istio/NGINX Ingress가 있고, 그 앞에 ALB가 있음

  • TLS가 두 번 종료될 수도 있음(외부 TLS 종료 + 내부 mTLS)
  • ALPN이 각 홉에서 재협상/변환될 수 있음

경로가 헷갈릴 때는, “외부에서 보이는 인증서”의 발급자/주체와 “파드가 실제로 사용하는 인증서”를 비교하면 도움이 됩니다.

인증서 체인/신뢰 오류가 섞여 있다면 이 글도 같이 보세요: EKS Pod에서 x509 unknown authority 오류 해결


3단계: TLS 1.3에서만 ALPN이 깨지는 대표 원인 6가지

원인 1) 서버(또는 Ingress)가 HTTP/2를 제대로 활성화하지 않음

예: NGINX Ingress에서 TLS는 되는데 h2가 비활성화된 경우

  • NGINX는 listen 443 ssl http2; 형태로 HTTP/2를 활성화합니다.
  • Ingress Controller 버전/설정에 따라 기본값이 다릅니다.

검증:

  • openssl s_client ... -alpn "h2"에서 ALPN protocol: h2가 안 나옴
  • curl --http2 실패

해결 방향:

  • Ingress/Proxy에서 HTTP/2 활성화
  • gRPC 서비스라면 Ingress가 gRPC 지원 모드인지 확인

원인 2) ALB/NLB/프록시가 TLS 1.3 + HTTP/2 조합에서 정책 불일치

ALB는 리스너 정책(SSL Policy)로 TLS 버전/암호군을 제어합니다.

  • 특정 SSL Policy가 TLS 1.3을 허용하지만, 백엔드/리다이렉트/헤더 처리에서 HTTP/2 경로가 꼬일 수 있습니다.
  • 또는 반대로, 클라이언트는 TLS 1.3을 제안하는데 리스너 정책이 TLS 1.2까지만 허용하여 실패할 수도 있습니다.

검증:

  • AWS 콘솔에서 ALB Listener의 Security policy 확인
  • CloudWatch/ALB access log에서 TLSNegotiationError 유사 패턴 확인

해결 방향:

  • 권장 SSL Policy로 변경(예: TLS 1.3 지원 정책)
  • HTTP/2 지원 여부(리스너/클라이언트) 재확인

원인 3) 클라이언트가 ALPN을 “필수”로 요구하는데 서버가 응답을 안 함

특히 gRPC 클라이언트는 h2가 없으면 실패합니다.

오류 예:

  • OpenSSL: no application protocol
  • gRPC: missing selected ALPN property

해결 방향:

  • 서버/프록시가 h2를 선택하도록 구성
  • 임시 우회로 HTTP/1.1로 폴백 가능한 클라이언트 옵션이 있는지 확인(가능하면 근본 해결 권장)

원인 4) OpenSSL/LibreSSL/boringssl 버전 차이로 TLS 1.3 핸드셰이크가 다르게 동작

EKS 파드 내부에서 사용하는 베이스 이미지가 오래된 경우,

  • OpenSSL 1.0.2: TLS 1.3 미지원
  • OpenSSL 1.1.1: TLS 1.3 지원(초기 버전엔 버그도 존재)
  • OpenSSL 3.x: 정책/기본값 변화(레거시 알고리즘 비활성 등)

진단:

# 파드 안에서
openssl version -a
curl --version

해결:

  • 베이스 이미지 업데이트
  • distroless 사용 시 curl/openssl 도구가 없으면 디버그용 ephemeral container 활용

원인 5) TLS 종료 지점이 바뀌면서 SNI/인증서 선택이 달라짐

TLS 1.3 자체 문제처럼 보이지만, 실제로는 SNI가 누락되거나 잘못된 인증서가 선택되어 실패하는 경우가 있습니다.

진단:

# SNI를 명시하지 않으면 다른 인증서가 나오는지 확인
openssl s_client -connect example.com:443 -tls1_3 -brief
openssl s_client -connect example.com:443 -servername example.com -tls1_3 -brief
  • SNI 없이 실패/성공이 갈리면 라우팅/인증서 선택 문제입니다.

원인 6) 중간 프록시가 HTTP/2를 HTTP/1.1로 변환하면서 헤더/업그레이드가 깨짐

ALB→Ingress→Service 구조에서, 앞단은 HTTP/2인데 뒷단은 HTTP/1.1로 내려가며 gRPC가 깨지는 케이스가 있습니다.

  • gRPC는 HTTP/2 프레이밍이 필요
  • “TLS 1.3만 실패”처럼 보이지만 실제로는 TLS 1.3을 쓰는 클라이언트가 HTTP/2를 더 적극적으로 사용하면서 문제가 드러나는 형태

간헐적 gRPC 실패가 동반된다면 이 글도 참고가 됩니다: EKS에서 gRPC 14 UNAVAILABLE 간헐 해결법


4단계: Kubernetes/EKS에서 실전 디버깅 루틴

1) 파드 내부에서 직접 목적지로 붙어보기

외부 경로(ALB 등)를 배제하고, 동일한 SNI/Host로 백엔드에 직접 붙어 문제가 재현되는지 봅니다.

# 임시 디버그 파드(네임스페이스는 상황에 맞게)
kubectl run -it --rm netdebug --image=curlimages/curl:8.5.0 -- sh

# 파드 내부에서
curl -v --http2 https://example.com/

만약 파드 내부에서는 TLS 1.3이 정상인데 외부에서만 실패한다면, 문제 지점은 대개 ALB/NLB/외부 프록시입니다.

2) Ingress/Service의 포트와 프로토콜을 다시 확인

  • Service가 443을 열고 있지만 실제 컨테이너는 8443을 듣는다든지
  • Ingress가 backend-protocol을 HTTP로 보내야 하는데 HTTPS로 보내는 등

특히 ALB Ingress를 쓴다면 annotation 하나로 동작이 크게 바뀝니다.

3) 서버가 선택한 ALPN을 서버 로그/메트릭으로 확인

  • Envoy: access log에 %PROTOCOL% / ALPN 관련 필드
  • NGINX: $server_protocol, $ssl_protocol, $ssl_alpn_protocol

NGINX 예시(커스텀 로그 포맷):

log_format tls '$remote_addr $host $ssl_protocol $ssl_cipher $ssl_alpn_protocol $server_protocol';
access_log /var/log/nginx/access.log tls;

TLS 1.3 요청에서 $ssl_alpn_protocol이 비어 있다면 협상이 안 된 것입니다.


해결 전략: “TLS 1.3을 끄기” 전에 해야 할 것들

운영에서 급하면 TLS 1.3을 임시로 끄는 선택을 하기도 합니다. 하지만 장기적으로는 원인을 제거하는 편이 낫습니다.

전략 1) ALPN(h2) 협상을 정상화

  • gRPC라면 서버/Ingress/프록시가 HTTP/2를 종단까지 지원하도록 구성
  • ALB에서 TLS 종료 후 백엔드가 HTTP/2를 필요로 한다면, 아키텍처를 재검토(예: NLB TCP 패스스루로 Ingress가 직접 TLS+HTTP/2 처리)

전략 2) TLS 종료 지점을 단순화

  • “ALB에서 TLS 종료 + Ingress에서 다시 TLS 종료”처럼 이중 TLS는 디버깅 난이도를 폭발시킵니다.
  • 가능하면 한 곳에서만 종료하거나, 각 홉의 역할을 명확히(외부 TLS, 내부 mTLS 등)

전략 3) 클라이언트/서버의 OpenSSL 스택 정렬

  • 오래된 OpenSSL(특히 1.1.1 초기)에서 TLS 1.3 관련 호환성 이슈가 보고된 적이 있습니다.
  • 컨테이너 베이스 이미지와 런타임을 최신으로 맞추고, curl/openssl도 함께 업데이트

전략 4) 인증서 체인과 SNI를 확실히

  • TLS 1.3에서만 실패처럼 보여도, 실제로는 특정 클라이언트가 체인 검증을 더 엄격히 하는 경우가 있습니다.
  • 중간 인증서 누락, 잘못된 체인 제공은 흔한 원인입니다.

체크리스트(요약)

  • openssl s_client로 TLS 1.2/1.3 각각에서 ALPN 선택 결과를 비교했다
  • curl --http2curl --http1.1HTTP/2 강제 시 실패 여부를 확인했다
  • TLS 종료 지점(ALB/NLB/Ingress/Pod)을 명확히 했다
  • gRPC라면 종단까지 HTTP/2가 유지되는지 확인했다(중간 변환 여부)
  • OpenSSL/curl 버전 차이를 확인했고, 오래된 이미지라면 업데이트했다
  • SNI를 명시했을 때/안 했을 때 인증서와 결과가 달라지는지 확인했다

결론

EKS에서 “TLS 1.3만 실패”는 겉보기와 달리 암호군의 문제가 아닌 경우가 많고, 실제 원인은 ALPN(특히 h2) 협상 실패, TLS 종료 지점의 불일치, 프록시/로드밸런서의 HTTP/2 처리, 클라이언트(OpenSSL) 스택 차이로 수렴하는 경우가 많습니다.

가장 빠른 접근은 OpenSSL로 -tls1_2/-tls1_3를 강제하고 -alpn "h2,http/1.1"로 협상 결과를 비교해 “TLS 문제인지, ALPN/HTTP2 문제인지”를 먼저 분리하는 것입니다. 그 다음 트래픽 경로를 TLS 종료 지점 기준으로 쪼개면, 문제를 재현 가능한 형태로 좁힐 수 있고(외부만/내부만), 결국 해결도 설정 한두 군데로 수렴합니다.