Haru Utils

TLS HANDSHAKE

TLS 인증서 만료·체인·SNI·프로토콜 장애

curl -k로 우회하지 않고 UTC 시각, SAN·만료일, 검증 체인, SNI별 leaf 인증서와 TLS 1.2·1.3 협상 결과를 분리해 확인한 뒤 NGINX 설정과 인증서 갱신을 안전하게 적용합니다.
certificate expiredunable to get local issuer certificateTLS SNI wrong certificatehostname mismatch SANTLS protocol version alertopenssl s_client verify_hostnameNGINX fullchain.pemCertbot renew failure
환경OpenSSL 3.x · curl · NGINX systemd · 선택적으로 Certbot · 공개 인증서와 제한된 private-key 경로 권한
분류웹·보안
검토일2026-09-04
진행5단계 · 조회 우선

SAFE OPERATING BOUNDARY

중단·복구 기준부터 확인하세요

STOP CONDITIONS

여기서는 멈추세요

  • DNS_NAME·실제 LB/backend·certificate 소유 팀과 UTC 시각을 확정하지 못했다면 갱신·config·reload를 실행하지 않습니다.
  • curl -k·--insecure 또는 verify error 무시로만 성공하는 경우 정상화로 간주하지 않고 trust chain 담당자에게 이관합니다.
  • private key·ACME token·client certificate secret의 출력·복사·외부 전달이 요구되면 작업을 중단하고 HSM·secret manager·승인된 배포 절차를 사용합니다.
  • TLS 1.0·1.1 활성화나 cipher 보안 수준 하향이 필요해 보이면 즉시 적용하지 말고 client 교체·위험 수용 승인을 포함한 보안 검토로 이관합니다.
ROLLBACK

복구 기준

NGINX server block·full-chain 경로·protocol 변경은 0600으로 보관한 정확한 이전 config를 nginx -t로 검증한 뒤 reload해 되돌립니다. 인증서 갱신은 임의 삭제하지 않고 CA·Certbot lineage에 보관된 검증된 이전 공개 certificate와 기존 private key를 표준 배포 절차로 다시 참조합니다. private key와 ACME credential은 rollback 증거에 포함하거나 출력하지 않습니다.

ESCALATION PACK

담당자에게 전달할 자료

  • UTC 시각·DNS 응답·관찰 지점, endpoint leaf subject/SAN/issuer/serial/notBefore/notAfter/SHA-256 fingerprint
  • verify return·chain build 결과와 backend IP별 SNI fingerprint, TLS 1.2·1.3·ALPN 협상 결과
  • 민감값을 제거한 NGINX server_name·certificate path·protocol diff와 nginx -t·reload 시각
  • Certbot dry-run/live renewal 결과 요약과 변경 전후 외부 health·client matrix 검증
2026년 9월 4일 기준 PostgreSQL·Kubernetes·OpenSSL·NGINX·Certbot 공식 문서를 대조했습니다. 진단은 읽기 전용 명령과 최소 권한 계정으로 시작하고, 취소·세션 종료·설정 적용·reload는 대상·영향·복구 경로를 기록해 담당자가 승인한 경우에만 실행합니다. <...> 자리표시자는 검토된 리터럴 값으로 직접 치환하며 외부 입력으로 shell 명령을 조립하지 않습니다.

BEFORE YOU START

이런 증상에서 시작합니다

  • certificate has expired 또는 certificate is not yet valid 오류가 발생함
  • unable to get local issuer certificate·unknown CA가 특정 client에서만 발생함
  • 같은 IP의 다른 도메인 인증서가 반환되거나 hostname mismatch가 발생함
  • protocol version·handshake failure가 특정 TLS client 또는 backend에서 발생함

CHECK THE BRANCH

놓치기 쉬운 원인 분기

01

만료·not yet valid인 경우

endpoint가 실제로 제공하는 leaf 인증서의 notBefore·notAfter와 UTC 시각을 비교합니다. 파일만 갱신되고 proxy가 이전 인증서를 계속 제공하는 상황을 구분합니다.

02

체인 검증 실패인 경우

leaf 다음 intermediate가 서버에서 제공되는지와 client trust store를 분리합니다. NGINX ssl_certificate에는 leaf가 먼저 오고 intermediate가 뒤따르는 full chain을 사용합니다.

03

SNI·hostname mismatch인 경우

DNS 이름을 -servername과 -verify_hostname에 모두 넣고 각 LB/backend IP에 연결해 비교합니다. IP로만 접속한 결과의 default certificate를 정상 인증서로 오판하지 않습니다.

04

프로토콜 협상 실패인 경우

TLS 1.2와 TLS 1.3을 각각 검증해 client/server 공통 범위를 확인합니다. 문제 해결을 위해 TLS 1.0·1.1이나 인증서 검증 우회를 켜지 않습니다.

FOLLOW THE FLOW

순서대로 확인하기

1
조회시스템을 변경하지 않는 확인 단계

UTC 시각·DNS·검증된 handshake 관찰

인증 정보나 client certificate를 넣지 않은 공개 health endpoint에서 시작합니다. curl --insecure 또는 -k를 사용하지 않으며 OpenSSL에는 검증 실패 시 중단 옵션을 넣습니다.

관찰 지점 UTC 시각
date -u '+%Y-%m-%dT%H:%M:%SZ'
DNS 주소
getent ahosts <DNS_NAME>
curl 인증서 검증
curl --fail --silent --show-error --output /dev/null --max-time 10 'https://<DNS_NAME>/<APPROVED_HEALTH_PATH>'
SNI·hostname·chain 검증 handshake
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -verify_hostname '<DNS_NAME>' -verify_return_error -brief </dev/null
결과 읽기

curl과 s_client가 모두 실패하면 인증서·chain·protocol 분기를, 특정 IP/backend에서만 실패하면 SNI routing 또는 배포 불일치를 우선 확인합니다.

다음 판단

endpoint가 제공하는 leaf 공개정보와 30일 만료 임계, 별도 chain 검증을 확인합니다.

2
조회시스템을 변경하지 않는 확인 단계

SAN·만료·issuer와 chain 판별

인증서는 공개 정보지만 내부 SAN과 topology가 포함될 수 있어 외부 공유 전 마스킹합니다. 이 단계의 s_client pipeline은 불신·만료 인증서에서도 공개 leaf 메타데이터를 수집하기 위해 verify_return_error를 사용하지 않는 진단 전용입니다. 비검증 수집 결과는 절대 정상 판정에 쓰지 않고 1단계의 별도 검증 handshake 결과와 함께 봅니다. private key 파일 내용은 어떤 명령에서도 출력하지 않습니다.

진단용 공개 leaf 인증서 정보
set -o pipefail
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -showcerts </dev/null | openssl x509 -noout -subject -issuer -serial -dates -ext subjectAltName -fingerprint -sha256
인증서 신뢰·hostname·chain 성공을 뜻하지 않는 비검증 메타데이터 수집입니다. 정상 판정은 별도의 -verify_hostname·-verify_return_error 명령으로만 합니다.
진단용 공개 leaf 30일 만료 확인
set -o pipefail
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -showcerts </dev/null | openssl x509 -checkend 2592000 -noout
x509 exit code 0은 수집한 leaf가 30일 뒤에도 날짜상 유효하고 1은 그 전에 만료됨을 뜻할 뿐, hostname·chain 신뢰 성공을 뜻하지 않습니다.
검토된 PEM chain 오프라인 검증
openssl verify -show_chain -CAfile <APPROVED_CA_BUNDLE> -untrusted <INTERMEDIATE_CHAIN_PEM> <LEAF_CERT_PEM>
leaf·intermediate·CA 공개 인증서 파일만 사용합니다. private key를 인수로 전달하지 않습니다.
NGINX 인증서 경로·권한 메타데이터
sudo nginx -T 2>&1 | grep -E '^[[:space:]]*(server_name|listen|ssl_certificate|ssl_certificate_key|ssl_protocols)'
sudo stat -Lc '%a %U:%G %n' <PRIVATE_KEY_PATH>
private key의 경로·권한만 확인하고 cat·head·checksum으로 내용을 출력하지 않습니다.
결과 읽기

SAN에 DNS 이름이 없으면 hostname 문제, notAfter 임박·경과면 갱신 문제, endpoint chain은 실패하지만 leaf 자체 날짜가 정상이라면 intermediate 제공 순서나 client CA bundle 문제입니다.

다음 판단

SNI별 backend와 TLS 1.2·1.3을 동일한 검증 조건으로 비교합니다.

3
주의서버 부하나 권한을 고려할 단계

SNI routing·backend 배포·프로토콜 범위 비교

LB 뒤 여러 IP를 하나씩 확인합니다. 신뢰 판정 명령은 -servername과 -verify_hostname을 실제 서비스 DNS 이름으로 유지합니다. 그 다음 별도 비검증 명령으로 공개 leaf fingerprint만 수집할 수 있지만 그 결과를 정상 판정에 사용하지 않습니다.

특정 LB·backend IP 신뢰 판정
openssl s_client -connect '<APPROVED_BACKEND_IP>:443' -servername '<DNS_NAME>' -verify_hostname '<DNS_NAME>' -verify_return_error -brief </dev/null
특정 LB·backend IP 공개 leaf 비교
set -o pipefail
openssl s_client -connect '<APPROVED_BACKEND_IP>:443' -servername '<DNS_NAME>' -showcerts </dev/null | openssl x509 -noout -subject -issuer -serial -dates -fingerprint -sha256
불신·만료 원인 비교를 위한 비검증 공개 메타데이터입니다. fingerprint가 같아도 hostname·chain 검증 성공을 의미하지 않습니다.
TLS 1.2 검증
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -verify_hostname '<DNS_NAME>' -verify_return_error -tls1_2 -brief </dev/null
TLS 1.3 검증
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -verify_hostname '<DNS_NAME>' -verify_return_error -tls1_3 -brief </dev/null
ALPN 협상 비교
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -verify_hostname '<DNS_NAME>' -verify_return_error -alpn 'h2,http/1.1' -brief </dev/null
인증서·NGINX 변경 전 설정 백업
install -d -m 0700 <APPROVED_EVIDENCE_DIR>
umask 077
if sudo test -f <NGINX_TLS_CONFIG>; then sudo cp --archive --no-clobber <NGINX_TLS_CONFIG> <APPROVED_EVIDENCE_DIR>/nginx-tls.before.conf; fi
if sudo test -f <APPROVED_EVIDENCE_DIR>/nginx-tls.before.conf; then sudo chmod 0600 <APPROVED_EVIDENCE_DIR>/nginx-tls.before.conf; fi
private key와 ACME account credential은 복사하지 않습니다. 백업 파일이 실제로 생성된 경우에만 chmod가 적용됩니다.
결과 읽기

backend별 fingerprint가 다르면 배포 불일치, DNS name handshake와 고정 IP+SNI 결과가 다르면 LB routing, 특정 protocol만 실패하면 승인된 client compatibility와 server policy 교집합 문제입니다.

다음 판단

인증서 갱신 또는 NGINX full-chain·protocol 설정 중 확인된 원인 하나만 변경합니다.

4
변경데이터·서비스 상태가 달라질 수 있는 단계

승인된 인증서 갱신 또는 NGINX TLS 설정 적용

CA rate limit·domain validation·LB 전체 배포 범위와 rollback certificate를 확인합니다. dry-run 성공 없이 live renewal하지 않고, nginx -t 성공 없이 reload하지 않습니다.

변경 단계입니다. 실행 전 대상 이름과 경로, 서비스 중단 영향, 복구 방법을 다시 확인하세요.
Certbot 관리 인증서 확인
sudo certbot certificates
도메인·경로는 내부 구성 정보일 수 있어 외부 공유 전에 마스킹합니다.
Certbot 갱신 dry-run
sudo certbot renew --cert-name <CERT_NAME> --dry-run
staging CA 검증과 authenticator·hook 성공을 확인합니다. 반복 live renewal로 CA rate limit을 소모하지 않습니다.
A. 승인된 인증서 갱신 1회
sudo certbot renew --cert-name <CERT_NAME>
만료 임계에 들어온 대상과 승인된 maintenance window에서 한 번 실행합니다. private key·ACME credential을 출력하지 않습니다.
B. 검토된 NGINX TLS config 적용
sudo install -o root -g root -m 0644 <REVIEWED_NGINX_TLS_CONFIG> <NGINX_TLS_CONFIG>
sudo nginx -t
sudo systemctl reload nginx
ssl_certificate가 leaf-first full chain을 가리키고 ssl_protocols 변경이 승인된 client matrix와 맞는지 확인합니다. private key 파일은 이 명령으로 교체하지 않습니다.
결과 읽기

갱신은 새 certificate lineage 제공, NGINX 변경은 chain 경로·SNI server block·protocol policy 적용입니다. 원인과 무관한 두 분기를 동시에 실행하지 않습니다.

다음 판단

외부 DNS와 각 backend에서 fingerprint·chain·SNI·TLS protocol을 다시 확인하고 실패하면 이전 config 또는 last-known-good certificate 참조로 원복합니다.

5
변경데이터·서비스 상태가 달라질 수 있는 단계

외부·backend 재검증과 NGINX 설정 롤백

DNS cache와 LB 전파를 고려해 최소 두 관찰 지점에서 검증합니다. 브라우저 한 대의 cache된 intermediate만으로 성공을 판단하지 않습니다.

변경 단계입니다. 실행 전 대상 이름과 경로, 서비스 중단 영향, 복구 방법을 다시 확인하세요.
외부 endpoint 최종 검증
curl --fail --silent --show-error --output /dev/null --max-time 10 'https://<DNS_NAME>/<APPROVED_HEALTH_PATH>'
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -verify_hostname '<DNS_NAME>' -verify_return_error -brief </dev/null
최종 공개 leaf 메타데이터 기록
set -o pipefail
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -showcerts </dev/null | openssl x509 -noout -subject -issuer -serial -dates -fingerprint -sha256
검증 명령 성공 후 증거용 공개 메타데이터를 기록합니다. 이 비검증 pipeline 자체를 성공 판정으로 사용하지 않습니다.
TLS 1.2·1.3 최종 검증
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -verify_hostname '<DNS_NAME>' -verify_return_error -tls1_2 -brief </dev/null
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -verify_hostname '<DNS_NAME>' -verify_return_error -tls1_3 -brief </dev/null
회귀 시 승인된 NGINX config 원복
sudo install -o root -g root -m 0644 <APPROVED_EVIDENCE_DIR>/nginx-tls.before.conf <NGINX_TLS_CONFIG>
sudo nginx -t
sudo systemctl reload nginx
이번 변경 전 백업이 존재하고 checksum·대상 경로·소유자 승인을 확인한 경우에만 실행합니다. private key는 변경하지 않습니다.
원복 후 hostname 검증
openssl s_client -connect '<DNS_NAME>:443' -servername '<DNS_NAME>' -verify_hostname '<DNS_NAME>' -verify_return_error -brief </dev/null
결과 읽기

정확한 SAN과 만료일, verify return 성공, 의도한 chain과 backend fingerprint 일치, 승인된 TLS 1.2·1.3 결과, HTTP health 성공이 모두 충족돼야 합니다.

다음 판단

NGINX config는 0600 백업으로 원복합니다. certificate lineage는 CA·Certbot의 검증된 이전 공개 certificate와 기존 private key 조합을 표준 배포 절차로 다시 참조하며, private key를 티켓·터미널 출력·임시 파일로 복사하지 않습니다.

PRIMARY REFERENCES

공식 문서

배포판과 버전에 따라 옵션·로그 위치가 다를 수 있습니다. 실행 전 서버의 --help와 로컬 매뉴얼을 함께 확인하세요.

도구 빠른 검색

최근 사용한 도구를 다시 열거나, 이름과 기능으로 검색하세요.

검색어와 도구의 입력·결과는 저장하지 않습니다.

↑↓ 이동 · Enter 열기 · Esc 닫기