서버는 자기 인증서 + 중간 CA 를 함께 보내야 한다.
루트 CA (브라우저·OS 에 미리 들어 있다)
└ 중간 CA ← 서버가 함께 보내야 하는 것
└ 리프 인증서 ← 우리 도메인 인증서
리프만 보내면 생기는 일
- 중간 CA 를 이미 캐시한 브라우저 — → 정상 동작한다
- 처음 오는 클라이언트 — → 검증 실패
- 서버 간 호출 (curl · SDK · 배치) — → 검증 실패
"브라우저는 되는데 서버에서 호출하면 인증서 오류" 의 전형이다 개발자 PC 에서는 재현이 안 되는 경우가 많아 원인을 늦게 찾는다
openssl s_client -connect example.com:443 -showcerts
# 체인이 몇 개 나오는지 본다. 1개면 중간 CA 가 빠진 것
-
fullchain.pem — 리프 + 중간 CA (nginx 의 ssl_certificate 에 이것을 준다)
-
cert.pem — 리프만 — 이것을 주면 위 문제가 생긴다
-
privkey.pem — 개인키 (ssl_certificate_key)
-
순서도 중요하다 — 리프가 먼저, 중간 CA 가 뒤
만료가 곧 장애다
- 자동 갱신을 건다 (ACME/Let's Encrypt 등)
- nginx 는 최근 ACME 모듈이 추가돼 외부 도구 없이 갱신하는 경로가 생겼다
- 갱신 후 반드시 reload 한다
- 파일만 바뀌고 프로세스가 옛 인증서를 들고 있는 상태가 실제로 자주 발생한다
- 만료 D-30 알림을 모니터링에 넣는다
openssl x509 -enddate -noout -in fullchain.pem echo | openssl s_client -connect example.com:443 2>/dev/null \
- | openssl x509 -noout -enddate # 실제 서비스 중인 것을 본다
파일이 아니라 서비스 중인 인증서를 확인한다. 갱신은 됐는데 reload 를 안 해서 만료된 것을 계속 내보내는 상태가 파일만 봐서는 안 보인다.
와일드카드의 범위
*.example.com 은 한 단계만 커버한다
a.example.com ✓
a.b.example.com ✗
다단계가 필요하면 *.b.example.com 을 따로 받거나 SAN 에 명시한다
SNI
- HTTPS 는 TLS 핸드셰이크가 먼저다
- Host 헤더를 읽기 전에 인증서를 골라야 한다
- 클라이언트가 SNI(Server Name Indication)로 호스트명을 평문으로 알린다
그래서 한 IP·한 포트에 여러 도메인의 인증서를 둘 수 있다 반대로 SNI 는 평문이라 '어느 사이트에 접속하는지' 는 관측 가능하다 (ECH 로 이를 가리려는 표준화가 진행 중이다)
만료 사고가 반복되는 이유
-
갱신은 자동인데 reload 가 수동이다
-
갱신 대상이 여러 곳인데 한 곳만 등록돼 있다
- (LB · nginx · 내부 서비스 간 mTLS 인증서가 각각 있다)
-
알림이 만료 D-1 이라 대응할 시간이 없다
-
담당자가 바뀌면서 갱신 스크립트의 존재를 아무도 모른다
-
기술 문제라기보다 운영 절차 문제다
-
만료일을 자산 목록으로 관리하고 D-30 알림을 건다
클라이언트 인증서(mTLS)
ssl_client_certificate /etc/ssl/ca.pem; # 클라이언트를 검증할 CA
ssl_verify_client on;
ssl_verify_depth 2;
proxy_set_header X-Client-DN $ssl_client_s_dn;
서버만 인증하는 일반 TLS 와 달리 양쪽이 서로를 인증한다 서비스 간 통신·파트너 연동에서 쓴다
운영 부담이 크다 — 클라이언트 인증서도 만료되고, 폐기 목록(CRL/OCSP) 관리가 붙는다 "보안이 좋으니 켜자" 로 시작하면 갱신 시점에 장애가 난다
면접 함정
- ❌ "인증서 파일 하나만 있으면 된다" → 중간 CA를 포함한 fullchain이 필요하다.
- ❌ "갱신됐으니 끝" → reload하지 않으면 옛 인증서를 계속 내보낸다.