- Published on
EKS Pod→RDS TLS 오류 - 인증서·SNI 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버는 멀쩡한데 EKS Pod에서만 RDS(Aurora 포함)로 붙을 때 TLS가 깨지는 경우가 있습니다. 대표적으로 x509: certificate signed by unknown authority, tls: handshake failure, no such host, certificate is not valid for any names 같은 메시지가 뜨고, 애플리케이션은 재시도만 하다가 장애로 번집니다.
이 글은 Pod → RDS 연결에서 TLS 오류가 나는 이유를 “인증서(Trust)·SNI(호스트명)·클라이언트 라이브러리 설정” 세 축으로 정리하고, 즉시 적용 가능한 체크리스트와 코드/명령 예제로 해결까지 안내합니다.
> 참고로, 쿠버네티스에서 x509 문제가 이미지 풀링 단계에서 발생하는 케이스는 원인이 다르지만(레지스트리 CA/프록시), 진단 방식은 유사합니다. 필요하면 Kubernetes ErrImagePull x509 인증서 오류 해결도 같이 보세요.
증상 패턴: “RDS는 되는데 Pod에서만 TLS가 실패”
다음 중 하나라도 해당하면 이 글의 범위입니다.
- 로컬/EC2에서는 RDS 접속 OK, EKS Pod에서만 실패
- 같은 VPC/서브넷인데도 애플리케이션만 TLS 실패(네트워크는 열려 있음)
- 에러 예시
x509: certificate signed by unknown authorityx509: cannot validate certificate for <ip> because it doesn't contain any IP SANscertificate verify failedtls: handshake failureSSL routines:ssl3_read_bytes:sslv3 alert handshake failure
핵심은 대부분 다음 중 하나입니다.
- 컨테이너의 CA 번들이 오래됨/없음 (RDS CA 회전, minimal image)
- SNI/hostname 검증이 깨짐 (IP로 접속, 잘못된 엔드포인트, 프록시 경유)
- 클라이언트의 TLS 옵션이 잘못됨 (verify 모드, serverName 미설정, 라이브러리 버그)
1) 가장 흔한 원인: 컨테이너에 CA 번들이 없거나 오래됨
왜 Pod에서만?
EKS Pod는 대개 distroless, alpine, scratch 같은 최소 이미지를 씁니다. 이런 이미지에는:
ca-certificates패키지가 아예 없거나- 오래된 CA 번들이 들어있거나
- 조직 프록시/사설 CA가 반영되지 않았거나
해서 RDS 서버 인증서를 신뢰하지 못합니다. 특히 AWS는 RDS/Aurora의 CA를 주기적으로 교체하며(예: rds-ca-2019 → rds-ca-rsa2048-g1 등), 오래된 번들을 가진 컨테이너에서 갑자기 장애가 납니다.
즉시 진단: Pod 안에서 인증서 체인 확인
아래는 busybox가 아니라 openssl/curl이 있는 디버그 Pod를 띄워 확인하는 방법입니다.
kubectl run -it --rm tls-debug \
--image=alpine:3.19 \
--command -- sh
# inside pod
apk add --no-cache openssl ca-certificates
update-ca-certificates
# RDS 엔드포인트로 SNI 포함하여 핸드셰이크
openssl s_client -connect mydb.cluster-xxxx.ap-northeast-2.rds.amazonaws.com:5432 \
-servername mydb.cluster-xxxx.ap-northeast-2.rds.amazonaws.com \
-showcerts </dev/null
출력에서 확인할 포인트:
Verify return code: 0 (ok)이어야 정상unable to get local issuer certificate/self signed certificate in certificate chain면 CA 문제 가능성이 큼
해결 1: 이미지에 ca-certificates 추가
Alpine
FROM alpine:3.19
RUN apk add --no-cache ca-certificates && update-ca-certificates
# app...
Debian/Ubuntu
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& update-ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Distroless
distroless는 패키지 설치가 어렵기 때문에 빌드 스테이지에서 CA 번들을 복사하는 패턴을 많이 씁니다.
FROM debian:bookworm-slim AS certs
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& update-ca-certificates
FROM gcr.io/distroless/base-debian12
COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# app...
해결 2: AWS RDS 글로벌 CA 번들(신규)로 명시
언어/드라이버에 따라 시스템 CA를 쓰지 않고 별도 CA 파일을 지정하는 게 더 안전할 때가 있습니다.
- AWS가 제공하는 RDS CA 번들을 ConfigMap/Secret으로 넣고
- 클라이언트에서
sslrootcert(Postgres),ssl-ca(MySQL) 같은 옵션으로 지정
예: PostgreSQL libpq(또는 psql)에서 CA 파일 지정
psql "host=mydb.cluster-xxxx.ap-northeast-2.rds.amazonaws.com port=5432 dbname=app user=app sslmode=verify-full sslrootcert=/etc/rds-ca/rds-combined-ca-bundle.pem"
K8s ConfigMap 예시:
apiVersion: v1
kind: ConfigMap
metadata:
name: rds-ca
data:
rds-combined-ca-bundle.pem: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
2) SNI/Hostname 검증 실패: “IP로 붙어서” 혹은 “호스트명이 바뀌어서”
TLS는 기본적으로 서버 인증서의 SAN(Common Name/SAN)과 접속 호스트명이 일치해야 합니다. RDS 인증서는 보통 *.rds.amazonaws.com 또는 특정 클러스터/인스턴스 FQDN에 맞춰 발급됩니다.
따라서 다음은 실패합니다.
- RDS 엔드포인트 대신 사설 IP로 직접 접속
- 잘못된 CNAME/프록시를 경유하면서 SNI가 원래 호스트명이 아닌 값으로 나감
- Aurora에서 writer/reader 엔드포인트를 혼동해 다른 호스트명으로 접속
대표 에러
x509: cannot validate certificate for 10.x.x.x because it doesn't contain any IP SANscertificate is not valid for any names, but wanted to match <host>
진단: 실제로 어떤 host로 연결 중인지 확인
애플리케이션 설정에서 다음을 점검하세요.
- 환경변수
DB_HOST가 FQDN인지, IP인지 - 서비스 디스커버리/템플릿이 바꾸지 않았는지
- 커넥션 스트링에
hostaddr(Postgres)처럼 IP를 강제하는 옵션이 들어갔는지
Postgres 예시(잘못된 케이스):
postgres://app:pw@10.0.12.34:5432/app?sslmode=verify-full
올바른 케이스:
postgres://app:pw@mydb.cluster-xxxx.ap-northeast-2.rds.amazonaws.com:5432/app?sslmode=verify-full
해결: SNI를 “RDS 엔드포인트 FQDN”으로 고정
언어별로 라이브러리가 SNI(server_name)를 제대로 설정하지 못하는 경우가 있어, 명시 설정이 필요할 때가 있습니다.
Node.js(pg) 예시
import pg from 'pg';
import fs from 'fs';
const { Pool } = pg;
const host = process.env.DB_HOST; // 반드시 RDS FQDN
const pool = new Pool({
host,
port: 5432,
database: 'app',
user: 'app',
password: process.env.DB_PASSWORD,
ssl: {
rejectUnauthorized: true,
// RDS CA 번들을 명시하면 환경 차이에 덜 흔들립니다.
ca: fs.readFileSync('/etc/rds-ca/rds-combined-ca-bundle.pem', 'utf8'),
// 일부 TLS 스택에서 SNI가 애매할 때 servername을 명시하면 도움이 됩니다.
servername: host,
},
});
Java(JDBC) PostgreSQL 예시
jdbc:postgresql://mydb.cluster-xxxx.ap-northeast-2.rds.amazonaws.com:5432/app?sslmode=verify-full&sslrootcert=/etc/rds-ca/rds-combined-ca-bundle.pem
> sslmode=verify-full은 호스트명 검증까지 수행합니다. 운영에서는 가능한 require보다 verify-full을 권장하지만, 그만큼 “호스트명/SNI가 정확해야” 합니다.
3) “TLS는 켰는데” 클라이언트 옵션 때문에 깨지는 케이스
(1) sslmode/verify 설정의 함정
- Postgres
sslmode=require: 암호화만, 호스트명 검증 안 함sslmode=verify-ca: CA만 검증sslmode=verify-full: CA + 호스트명까지 검증
보안적으로는 verify-full이 맞지만, 설정이 조금이라도 틀리면 장애가 납니다. 특히 아래 조합이 자주 문제를 만듭니다.
verify-full인데DB_HOST가 IPverify-full인데 CA 파일을 잘못 지정- 컨테이너에 CA 번들이 없는데
rejectUnauthorized=true
(2) 최소 이미지에서 시간/타임존 문제
TLS 검증은 인증서의 NotBefore/NotAfter를 보는데, 컨테이너 시간이 틀어져 있으면(드물지만) “아직 유효하지 않음/만료”가 뜰 수 있습니다.
진단:
date -u
노드 시간 동기화(NTP/chrony)나 런타임 이슈를 점검합니다.
(3) 프록시/서비스메시가 TLS를 가로채는 경우
Envoy/Istio/프록시가 egress TLS를 중간에서 처리하면서 SNI/ALPN/검증 정책이 바뀌면 RDS 접속이 실패할 수 있습니다. 이때는 다음을 확인합니다.
- egress 정책이 DB 포트를 TLS로 “재암호화”하는지
- 원본 SNI를 유지하는지
- 프록시가 사설 CA로 MITM을 하는지(보안 정책)
gRPC/프록시 계열 트러블슈팅 방식은 유사하니, 네트워크 계층의 리셋/언어별 타임아웃이 얽혀 있다면 Kubernetes gRPC UNAVAILABLE·RST_STREAM 원인과 Envoy·NGINX 대응도 도움이 됩니다.
4) 재현 가능한 “현장 체크리스트”
아래 순서대로 보면 보통 30분 안에 원인을 좁힐 수 있습니다.
4.1 네트워크가 아니라 TLS인지 먼저 확정
# TCP 연결 확인 (Postgres 5432, MySQL 3306)
kubectl exec -it <pod> -- sh -c "nc -vz mydb.cluster-xxxx.ap-northeast-2.rds.amazonaws.com 5432"
succeeded면 보안그룹/NACL/라우팅은 대체로 OK- 여기서 막히면 TLS 이전 문제(보안그룹, DNS, CNI 등)
4.2 SNI 포함 openssl로 검증
kubectl exec -it <pod> -- sh -c "openssl s_client -connect mydb.cluster-xxxx.ap-northeast-2.rds.amazonaws.com:5432 -servername mydb.cluster-xxxx.ap-northeast-2.rds.amazonaws.com </dev/null | tail -n +1"
Verify return code확인- 인증서 체인이 어떤 CA로 이어지는지 확인
4.3 컨테이너 CA 번들 상태 확인
kubectl exec -it <pod> -- sh -c "ls -l /etc/ssl/certs/ca-certificates.crt || true"
없으면 거의 확정적으로 CA 설치/복사가 필요합니다.
4.4 애플리케이션이 실제로 어떤 host로 붙는지 로그로 남기기
- 커넥션 스트링을 그대로 로그에 찍지 말고(비번 노출),
- host/port/dbname/sslmode만 구조화 로그로 남깁니다.
예: Node
console.log({
dbHost: process.env.DB_HOST,
dbPort: process.env.DB_PORT,
sslMode: process.env.DB_SSLMODE,
});
5) 운영에서 안전하게 굳히는 방법(재발 방지)
5.1 CA 번들 배포를 “이미지 빌드”가 아니라 “런타임 마운트”로 표준화
- CA 회전 시 이미지 재빌드 없이 대응 가능
- 멀티런타임(Go/Java/Node)에서 동일한 경로로 통일
Deployment 예시:
volumeMounts:
- name: rds-ca
mountPath: /etc/rds-ca
readOnly: true
volumes:
- name: rds-ca
configMap:
name: rds-ca
5.2 “항상 FQDN으로만 접속” 규칙화
- IP로의 직접 접속 금지
- Aurora는 writer/reader 엔드포인트를 명확히 분리
- 커넥션 풀/프록시 도입 시에도 원본 호스트명 유지
DB 연결이 불안정해질 때는 TLS만 보지 말고 커넥션 폭주도 같이 점검해야 합니다. 특히 장애 시 재시도가 폭증하면 remaining connection slots are reserved로 2차 장애가 납니다. 이 주제는 Aurora PostgreSQL remaining connection slots are reserved로 서비스가 멈출 때 RDS Proxy와 pgBouncer와 max_connections 튜닝으로 커넥션 폭주를 영구 차단하는 실전 체크리스트에서 더 깊게 다뤘습니다.
5.3 절대 해서는 안 되는 임시방편
rejectUnauthorized: false(Node)sslmode=disable(Postgres)- JVM에서 truststore 검증을 통째로 끄기
장애는 잠깐 사라지지만, 중간자 공격과 자격 증명 탈취에 그대로 노출됩니다. TLS 문제는 “검증을 끄는 것”이 아니라 “검증이 성공하도록 신뢰 사슬과 SNI를 맞추는 것”이 정답입니다.
6) 결론: TLS 오류는 대부분 “CA” 아니면 “SNI”다
EKS Pod → RDS TLS 오류는 복잡해 보이지만, 실제로는 다음 두 줄로 요약됩니다.
- CA(신뢰 루트)가 컨테이너에 없거나 낡았다 →
ca-certificates/RDS CA 번들로 해결 - 접속 호스트명(SNI/hostname verification)이 틀렸다 → IP 접속 금지, RDS FQDN 고정, 필요 시
servername명시
위 체크리스트대로 openssl s_client -servername로 먼저 사실관계를 확보하고, (1) CA 번들 정비 (2) FQDN/SNI 정합성 확보 (3) 드라이버 옵션 정리까지 하면 재발률이 크게 줄어듭니다.