이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 인증서 chain, SAN, hostname 검증은 각각 무엇을 확인하는가?
  • SNI 누락과 잘못된 Host 설정이 왜 특정 환경에서만 장애를 만드는가?
  • 인증서 발급과 갱신을 인프라 팀과 어떤 정보로 맞춰야 하는가?

개요

인증서는 “이 서버가 이 hostname을 대표해도 된다”는 증명서다. 하지만 서버 인증서 하나만 있으면 끝나는 구조가 아니다. 클라이언트는 leaf 인증서에서 중간 인증서를 거쳐 신뢰 가능한 root CA까지 이어지는 chain을 검증하고, 접속 hostname이 SAN에 포함되어 있는지도 확인한다.

  • CN보다 SAN이 hostname 검증의 중심이다.
  • wildcard 인증서는 보통 *.example.com 한 단계에만 맞고 a.b.example.com까지 자동으로 덮지 않는다.
  • SNI는 client가 TLS handshake 중 접속하려는 hostname을 알려 주는 확장이다.
  • 내부 API TLS에서는 사설 CA, JVM truststore, container image의 CA bundle 차이가 흔한 장애 원인이다.

원리

인증서 검증은 크게 네 가지 질문이다.

  • 지금 받은 인증서가 접속 hostname에 유효한가?
  • 인증서가 만료되지 않았고 key usage가 맞는가?
  • leaf부터 root까지 chain이 끊기지 않는가?
  • root 또는 intermediate CA를 client가 신뢰하는가?

서버는 보통 leaf 인증서와 intermediate 인증서를 함께 제공한다. root 인증서는 클라이언트 trust store에 이미 있어야 한다. intermediate가 누락되면 최신 브라우저는 보완해 주기도 하지만, Java, 오래된 모바일, CLI, 일부 사내 프록시는 그대로 실패할 수 있다.

SNI가 필요한 이유

하나의 IP와 443 포트 뒤에 여러 도메인이 붙는 것은 흔하다. 서버는 TLS handshake 초기에 HTTP Host header를 아직 볼 수 없으므로, client가 ClientHello의 SNI에 hostname을 담아 보내야 올바른 인증서를 고를 수 있다.

ClientHello(server_name=api.example.com)
  -> LB chooses certificate for api.example.com
  -> ServerHello + Certificate(SAN=api.example.com)

SNI가 없으면 LB는 기본 인증서를 내려줄 수 있다. 그래서 브라우저는 정상인데 구형 client, 잘못 만든 health checker, 내부 스크립트만 인증서 mismatch를 겪는 일이 생긴다.

백엔드 호출에서의 함정

백엔드가 외부 API나 내부 서비스를 호출할 때는 브라우저보다 더 엄격하거나 더 오래된 trust 환경을 사용할 수 있다.

  • container image에 최신 CA bundle이 없다.
  • JVM truststore에 사설 CA가 없다.
  • 호출 URL은 https://10.0.1.12인데 인증서 SAN은 payment.internal만 가진다.
  • proxy가 TLS를 가로채며 사내 CA로 재서명하지만 애플리케이션 truststore에는 해당 CA가 없다.
  • staging과 production의 인증서 chain이 다르다.

Java의 PKIX path building failed는 “서버가 죽었다”가 아니라 “내 JVM이 이 인증서 chain을 신뢰하지 못한다”에 가깝다.

오류별 판단 기준

증상더 가까운 원인확인 포인트
PKIX path building failedtruststore가 chain을 신뢰하지 못함JVM CA, intermediate, 사내 CA
hostname verification failedSAN과 접속 hostname 불일치URL host, SAN, wildcard 범위
브라우저 정상, Java 실패client truststore 차이container/JDK CA bundle
SNI 없는 client만 실패기본 인증서 반환openssl s_client SNI 포함/미포함 비교
특정 파트너만 실패pinning 또는 구형 TLS stack인증서 교체 공지, fingerprint, runtime

이 표를 쓰면 “인증서 문제”를 발급 문제, 설치 문제, 신뢰 저장소 문제, client 구현 문제로 나눌 수 있다.

코드와 명령으로 확인하기

SNI와 chain을 함께 본다.

openssl s_client -connect api.example.com:443 \
  -servername api.example.com \
  -showcerts </dev/null

SNI를 일부러 빼서 기본 인증서가 무엇인지 비교한다.

openssl s_client -connect api.example.com:443 </dev/null

DNS를 바꾸지 않고 특정 IP의 인증서를 검증한다.

curl -v --resolve api.example.com:443:203.0.113.10 https://api.example.com/health

인증서 만료일만 빠르게 확인한다.

echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

인증서 운영 체크리스트

  • 발급 대상 hostname 목록과 SAN을 앱 팀, 인프라 팀, 보안 팀이 같은 표로 관리한다.
  • wildcard를 쓸 때는 포함 범위와 제외되는 하위 도메인을 명확히 적는다.
  • 갱신 자동화는 발급 성공뿐 아니라 LB/CDN/proxy 반영, reload, 외부 검증까지 포함한다.
  • private CA를 쓴다면 발급자, 배포 경로, truststore 반영 방식, 폐기 절차를 문서화한다.
  • B2B API는 파트너사가 certificate pinning을 하는지 확인한다. pinning이 있으면 인증서 교체가 장애가 될 수 있다.

인프라 협업 포인트

인증서 요청을 할 때는 다음 정보를 한 번에 전달하면 왕복이 줄어든다.

  • 도메인과 SAN 목록
  • 공개 인증서인지 내부 인증서인지
  • 적용 위치가 CDN, LB, ingress, app server 중 어디인지
  • SNI 기반 multi cert가 필요한지
  • 갱신 주체와 만료 알림 채널
  • 검증 명령과 기대 출력

인프라 팀의 고충은 “인증서만 바꿔 주세요”라는 요청 뒤에 숨은 의존성이 많다는 점이다. DNS 검증, CA 권한, 보안 승인, maintenance window, 파트너사 pinning, rollback 경로가 모두 연결되어 있다.

주니어가 자주 하는 오해

  • 인증서 파일 하나만 설치하면 chain 검증이 끝난다고 생각한다.
  • wildcard 인증서가 모든 깊이의 하위 도메인을 덮는다고 생각한다.
  • 브라우저에서 정상이라고 서버 사이드 Java 호출도 정상일 것이라 생각한다.
  • 인증서 자동 갱신 성공을 실제 서비스 반영 성공과 같은 의미로 본다.

시니어의 설계 판단 기준

  • 공개 인증서, 내부 인증서, 사설 CA, mTLS client cert의 소유자와 갱신 경로를 나눈다.
  • SAN 목록을 코드의 callback URL, webhook URL, service discovery 이름과 함께 검토한다.
  • 인증서 교체는 발급, 배포, reload, 외부 검증, rollback, 파트너 공지를 하나의 변경으로 다룬다.
  • pinning이 있는 B2B API에서는 인증서 교체 리드타임을 일반 웹보다 길게 잡는다.

실전 팁

  • 인증서 장애는 “전체 장애”보다 “특정 client 장애”로 시작하는 일이 많다. client 종류, OS, Java version, 사내망 여부를 먼저 묻는다.
  • cert-manager, ACME, cloud managed cert를 쓰더라도 만료 모니터링과 실제 endpoint 검증은 별도로 둔다.
  • 내부 서비스는 IP로 호출하지 말고 인증서 SAN에 맞는 DNS 이름으로 호출한다.
  • 인증서 교체 전후로 fingerprint가 바뀌는 것은 정상일 수 있지만 issuer, SAN, chain 길이, 만료일이 기대와 맞는지 확인한다.
  • 개인 프로젝트는 Let’s Encrypt 자동 갱신 후 nginx -s reload나 reverse proxy reload가 실제로 되었는지 검증해야 한다.

위험 신호!

  • 인증서가 “어느 장비에” 설치되어 있는지 앱 팀이 모른다.
  • SAN 목록이 코드의 callback URL, webhook URL, 내부 호출 hostname과 맞는지 확인하지 않는다.
  • 내부 CA root를 개발자 노트북에는 넣었지만 운영 container에는 넣지 않았다.
  • 인증서 만료 알림이 개인 이메일로만 간다.

확인 질문

확인 질문

  • SNI 없이 TLS 요청을 보내면 어떤 문제가 생길 수 있는가?
    • 같은 IP에서 여러 인증서를 쓰는 환경에서 서버가 기본 인증서를 내려줘 hostname mismatch가 날 수 있다.
  • intermediate 인증서가 누락되면 왜 일부 client만 실패할 수 있는가?
    • client마다 누락된 intermediate를 보완하는 방식과 truststore 구성이 다르기 때문이다.
  • 내부 API를 IP로 호출하면 TLS에서 어떤 문제가 생기기 쉬운가?
    • 인증서 SAN은 보통 DNS 이름을 포함하므로 IP 호출은 hostname 검증에 실패할 수 있다.

참고 문서