웹사이트가 “안전하지 않음” 경고를 띄우거나 API 클라이언트가 SSL/TLS 오류로 실패할 때, 가장 흔한 원인은 두 가지입니다. 바로 인증서 체인이 불완전하거나, 인증서의 도메인 정보가 접속하려는 호스트와 불일치하는 문제입니다. 이 가이드는 실제 운영 환경에서 바로 적용할 수 있는 명령어, 서버별 설정 예시, 체크리스트를 통해 두 문제를 빠르게 진단하고 해결하는 방법을 단계별로 안내합니다.
핵심 요약: 왜 이 문제가 중요한가
- 인증서 체인 불완전: 서버가 중간 인증서를 제대로 제공하지 않아, 클라이언트가 신뢰 경로를 완성하지 못하는 상태.
- 도메인 불일치: 인증서의 CN 또는 SAN(Subject Alternative Name)에 접속 도메인이 없어 호스트네임 검증에 실패.
두 문제는 브라우저 경고(“이 연결은 완전히 안전하지 않습니다”), curl/openssl 오류(“unable to get local issuer certificate”, “certificate verify failed”, “host name mismatch”)로 드러납니다. 검색 순위 하락, 전환율 감소, API 통신 장애 등 사업적 리스크가 크므로 즉시 조치가 필요합니다.
기본 개념 정리: 인증서 체인과 도메인 일치
인증서 체인(Trust Chain)이란?
- Leaf(서버) 인증서: 여러분의 도메인에 발급된 인증서.
- Intermediate(중간) 인증서: 루트 인증서와 서버 인증서를 연결하는 사슬.
- Root(루트) 인증서: OS/브라우저의 신뢰 저장소에 기본 탑재된 신뢰 anchor.
서버는 일반적으로 “서버 인증서 + 중간 인증서(들)”을 함께 제공해야 하며, 루트 인증서는 제공하지 않습니다(클라이언트가 자체 보유).
도메인 일치(Hostname Validation)
- 인증서의 CN 또는 SAN 목록에 접속하는 호스트명이 반드시 포함되어야 합니다.
- 와일드카드 예: *.example.com은 www.example.com, api.example.com은 커버하지만 example.com은 커버하지 않습니다.
- IP 주소 접속 시: 인증서의 SAN에 해당 IP가 표기돼 있어야 합니다. 대부분의 공인 인증서는 IP SAN을 포함하지 않습니다.
빠른 진단 체크리스트
다음 중 하나라도 해당한다면, 아래 단계별 해결을 진행하세요.
- 브라우저: ERR_CERT_AUTHORITY_INVALID, ERR_CERT_COMMON_NAME_INVALID 표시
- curl: SSL certificate problem: unable to get local issuer certificate / certificate verify failed
- openssl: verify error:num=20: unable to get local issuer certificate, or hostname mismatch
- 특정 OS/브라우저/모바일에서만 오류 발생(중간 인증서 혹은 체인 선택 문제 가능성 높음)
- www 유입은 정상이나 apex(루트 도메인) 또는 특정 서브도메인만 실패(도메인 불일치 가능성)
1부. 인증서 체인 불완전 문제: 증상, 원인, 해결
흔한 오류 메시지
- 브라우저: ERR_CERT_AUTHORITY_INVALID, “인증서가 신뢰되지 않습니다”
- curl:
- SSL certificate problem: unable to get local issuer certificate
- error:0A00018E:SSL routines::ca md too weak (환경에 따라)
- openssl verify: error 20 at 0 depth lookup: unable to get local issuer certificate
1단계: 현재 체인 상태 확인
다음 명령으로 서버가 제공하는 인증서와 체인을 확인합니다.
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | openssl x509 -noout -subject -issuer -dates
- 여러 개의 인증서 블록이 출력됩니다. 순서가 Leaf → Intermediate(들)인지 확인하세요.
- issuer가 다음 인증서의 subject와 매칭되는지 확인합니다.
또는 SAN과 issuer 확인:
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | openssl x509 -noout -subject -issuer -ext subjectAltName
SSLLabs 서버 테스트를 활용하면 체인 문제, 호환성, TLS 설정을 한 번에 확인할 수 있습니다.
2단계: 올바른 중간 인증서 확보
문제의 대부분은 “중간 인증서 파일 누락” 혹은 “잘못된 중간/체인 선택”에서 발생합니다. 해결을 위해 다음을 수행하세요.
- 인증서를 발급한 CA의 공식 문서에서 해당 서버 인증서에 맞는 최신 중간 인증서를 다운로드합니다.
- Let’s Encrypt를 사용한다면 보통 fullchain.pem이 서버 인증서 + 중간 인증서를 포함합니다.
- 오래된 체인이나 교차서명(cross-signed) 체인을 잘못 선택하면 특정 기기(특히 구형 Android)에서만 실패할 수 있습니다. CA에서 권장하는 최신 체인을 사용하세요.
3단계: 서버별 올바른 체인 설치
서버/로드밸런서/프록시별로 체인을 지정하는 방식이 다릅니다.
-
Nginx
-
ssl_certificate: 서버 인증서 + 중간 인증서가 포함된 fullchain.pem 지정
-
ssl_certificate_key: 프라이빗 키
-
예:
server { listen 443 ssl http2; server_name example.com www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # (선택) OCSP Stapling 안정성을 위해 ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; }
-
-
Apache (2.4.8 이상)
-
SSLCertificateFile에 fullchain.pem 지정, SSLCertificateKeyFile에 키 지정.
-
예:
<VirtualHost *:443> ServerName example.com SSLEngine on SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem </VirtualHost> -
Apache 2.4.7 이하에서는 SSLCertificateChainFile을 별도로 사용합니다.
-
-
HAProxy
-
하나의 PEM 파일에 private key + fullchain을 순서대로 결합:
cat /etc/letsencrypt/live/example.com/privkey.pem \ /etc/letsencrypt/live/example.com/fullchain.pem \ > /etc/haproxy/certs/example.com.pem -
frontend에 crt 또는 crt-list로 지정.
-
-
IIS (Windows)
-
PFX로 가져오기: 프라이빗 키와 체인을 포함한 PFX를 만듭니다.
openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out example.pfx -name "example.com" -
“컴퓨터 계정 > 개인” 스토어로 가져온 뒤 사이트 바인딩에서 해당 인증서 선택. SNI 사용 시 도메인별 바인딩 필요.
-
-
AWS ALB/ELB/CloudFront
- AWS Certificate Manager(ACM)에 “인증서 + 체인 + 키”를 업로드(가져오기)하거나 ACM에서 발급받습니다.
- ALB 리스너에 해당 인증서를 연결합니다.
- CloudFront는 us-east-1(버지니아 북부) 리전에 있는 ACM 인증서만 선택 가능하며, 배포 Alternate Domain Names(CNAME)에 도메인이 모두 포함되어야 합니다.
-
Java/Tomcat
-
PKCS#12를 생성 후 Keystore로 등록:
openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out example.p12 -name example keytool -importkeystore -destkeystore keystore.jks -srckeystore example.p12 -srcstoretype PKCS12 -alias example -
server.xml에서 keystore 지정을 확인합니다.
-
중요: 반드시 “서버 인증서 → 중간 인증서(들)” 순서로 연결되어야 하며, 루트 인증서는 포함하지 않습니다.
4단계: 재기동 및 검증
-
서비스를 재시작한 후 다음 명령으로 재확인:
curl -vI https://example.com echo | openssl s_client -connect example.com:443 -servername example.com -showcerts -
SSLLabs 테스트에서 체인 문제 경고가 사라졌는지 확인합니다.
5단계: 까다로운 사례와 회피 전략
- 교차서명 문제: 과거 Let’s Encrypt의 DST Root CA X3 만료처럼, 구형 클라이언트 호환을 고려해야 할 때가 있습니다. CA에서 권장하는 최신 체인으로 전환하세요.
- 특정 기기에서만 실패: 해당 기기의 신뢰 저장소에 루트가 없거나, 서버가 구식 체인을 제공하는 경우가 많습니다. 체인을 최신화하세요.
- 프록시/중간 장비: TLS 오프로드를 수행하는 장비(로드밸런서, CDN, IDS 등)에도 동일하게 올바른 체인을 설정해야 합니다.
2부. 도메인 불일치 문제: 증상, 원인, 해결
흔한 오류 메시지
- 브라우저: ERR_CERT_COMMON_NAME_INVALID, “호스트명이 인증서와 일치하지 않습니다”
- curl: SSL: no alternative certificate subject name matches target host name 'api.example.com'
- Java: javax.net.ssl.SSLPeerUnverifiedException: Hostname verification failed
1단계: 접속 호스트와 인증서 SAN 비교
다음 명령으로 인증서의 SAN 목록을 확인합니다.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
- SAN에 접속하려는 호스트명이 없는지 확인합니다.
- 와일드카드 *.example.com은 example.com(루트 도메인)을 커버하지 않습니다. 필요한 모든 호스트를 SAN에 추가해야 합니다.
2단계: SNI(Server Name Indication)와 라우팅 확인
같은 IP/포트에 여러 도메인을 호스팅할 때는 SNI가 필수입니다. 다음으로 점검하세요.
-
테스트 시 -servername 옵션을 반드시 지정:
echo | openssl s_client -connect 203.0.113.10:443 -servername www.example.com -
curl로 특정 IP에 도메인을 강제 맵핑해 실제 라우팅을 재현:
curl -vI --resolve www.example.com:443:203.0.113.10 https://www.example.com
위 테스트에서 반환되는 인증서의 CN/SAN이 기대와 다르면, 가상호스트 설정 또는 로드밸런서의 도메인-인증서 매핑이 잘못되었을 가능성이 큽니다.
3단계: 서버/프록시별 도메인 매핑 수정
-
Nginx
-
server_name으로 정확한 도메인을 지정하고, 각 서버 블록에 올바른 인증서를 매핑:
server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/ssl/api.example.com/fullchain.pem; ssl_certificate_key /etc/ssl/api.example.com/privkey.pem; } server { listen 443 ssl http2; server_name www.example.com; ssl_certificate /etc/ssl/www.example.com/fullchain.pem; ssl_certificate_key /etc/ssl/www.example.com/privkey.pem; } -
default_server에 잘못된 인증서가 걸려 있으면 SNI 미지원 클라이언트가 틀린 인증서를 받을 수 있습니다. 기본 서버는 444 반환 등으로 안전하게 처리하세요.
-
-
Apache
- 도메인별 VirtualHost에 각각 올바른 SSLCertificateFile/KeyFile 설정.
- ServerName/ServerAlias가 실제 DNS와 일치하는지 점검.
-
HAProxy
-
crt-list로 도메인별 인증서 지정 또는 SNI 기반 라우팅:
frontend https-in bind :443 ssl crt-list /etc/haproxy/crt-list.txt
-
-
AWS ELB/ALB
- 리스너에 연결된 인증서가 해당 도메인을 커버하는지 확인.
- 다수의 도메인을 같은 리스너에서 처리할 경우 SNI 인증서(멀티 도메인 또는 와일드카드)를 연결.
-
CloudFront
- Alternate Domain Names(CNAME)에 도메인 추가, 해당 도메인을 커버하는 ACM 인증서 선택.
- DNS CNAME 레코드가 CloudFront 도메인을 가리키는지 점검.
4단계: 인증서 재발급 또는 SAN 확장
-
필요한 모든 호스트명을 SAN에 포함해 재발급하세요.
-
Let’s Encrypt 예시:
sudo certbot certonly --nginx -d example.com -d www.example.com -d api.example.com -
와일드카드가 유효한 경우:
sudo certbot certonly --dns-cloudflare -d "*.example.com" -d example.com
주의: 와일드카드는 서브도메인만 커버하며 루트 도메인은 별도로 포함해야 합니다.
5단계: 특수 사례
- IP 주소로 접속: 인증서 SAN에 IP를 추가하지 않았다면 호스트 검증이 실패합니다. FQDN 기반 접속으로 전환하거나, 내부망이라면 사설 CA로 FQDN 인증서를 발급하세요.
- IDN(국제화 도메인): 인증서에는 퓨니코드(xn-- 형태)로 SAN이 기록됩니다. 발급 시 퓨니코드 변환이 올바른지 확인하세요.
- 역프록시/백엔드 연동: 프록시가 Host 헤더를 보존하지 않으면 백엔드의 SNI/가상호스트 라우팅이 꼬일 수 있습니다. 예: Nginx에서 proxy_set_header Host $host; 설정 유지.
실전 예시 1: Let’s Encrypt + Nginx 체인 불완전 문제 해결
증상: 모바일 브라우저에서만 “인증서를 확인할 수 없습니다” 경고 발생.
해결:
- 확인
echo | openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts
중간 인증서가 누락되어 있음을 확인.
- fullchain.pem 사용하도록 Nginx 설정 수정
ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
- 재시작 후 SSLLabs로 재검증. 모든 클라이언트에서 신뢰됨을 확인.
실전 예시 2: 멀티 도메인 사이트의 도메인 불일치 해결(AWS ALB)
증상: api.example.com은 정상이나 admin.example.com은 ERR_CERT_COMMON_NAME_INVALID.
원인: ALB 리스너에 연결된 인증서가 api.example.com만 포함.
해결:
- ACM에서 멀티 도메인 인증서 발급(SAN에 api.example.com, admin.example.com 모두 포함).
- ALB 리스너 인증서를 새 인증서로 교체.
- curl --resolve로 IP 강제 후 검사:
curl -vI --resolve admin.example.com:443:ALB_IP https://admin.example.com
SAN 포함 및 정상 응답을 확인.
실전 예시 3: 사내 내부망(IP 접속) API의 호스트 검증 실패
증상: 내부 API를 https://10.0.0.5로 호출 시 Java 클라이언트에서 SSLPeerUnverifiedException.
해결 방향:
- FQDN 내부 도메인(api.internal.local)을 사설 DNS에 등록하고, 사설 CA로 해당 FQDN에 대한 인증서 발급.
- 클라이언트는 사설 CA 루트를 신뢰 저장소에 추가.
- IP 직접 접속 대신 FQDN 접속으로 전환.
장애 재현과 원인 추적에 유용한 명령어 모음
- 서버 인증서/체인 확인
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts
- SAN, subject, issuer, 만료일 확인
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
- 체인 검증
openssl verify -CAfile chain.pem server-cert.pem
- 특정 IP로 도메인 연결하여 테스트
curl -vI --resolve www.example.com:443:203.0.113.10 https://www.example.com
- 엄격 검증으로 요청(로컬 CA 번들 지정)
curl --cacert /etc/ssl/certs/ca-bundle.crt https://example.com -v
운영 환경별 체크리스트
- 웹서버/프록시/로드밸런서 모든 경로에 동일하게 올바른 체인을 배치했는가?
- 서버 인증서 파일이 fullchain(서버+중간)인지 확인했는가?
- 루트 인증서를 의도치 않게 함께 제공하지 않는가?
- SAN에 필요한 모든 호스트명이 포함되어 있는가(루트, www, api 등)?
- 와일드카드 사용 시 루트 도메인을 별도로 포함했는가?
- SNI 매핑이 올바른가(가상호스트, crt-list, ALB 리스너 설정)?
- 역프록시가 Host 헤더를 보존하는가?
- 만료일을 모니터링하고 자동 갱신(ACME, Certbot 등)이 설정되어 있는가?
예방 전략과 베스트 프랙티스
-
자동화
- ACME 기반 자동 갱신(Certbot, lego, acme.sh)을 도입하고, 갱신 후 웹서버 재로드를 자동화하세요.
- CI/CD 파이프라인에서 배포 전 openssl/curl 검증 스텝을 추가합니다.
-
모니터링
- 인증서 만료 모니터링(예: Prometheus exporter, Cron + 메일 알림, 사내 대시보드).
- SSLLabs, Hardenize, Censys 등을 통한 정기 점검.
- Certificate Transparency(CT) 로그 모니터링으로 의도치 않은 발급 탐지.
-
호환성과 보안
- 최신 중간 인증서를 사용하고, 구형 클라이언트 호환이 필요한 경우 CA 권고 체인을 따릅니다.
- 강력한 암호 스위트와 최신 프로토콜(TLS 1.2/1.3)을 우선 적용.
- HSTS를 신중히 설정하여 중간자 공격 위험을 감소(도메인 일치 및 체인 문제가 모두 해결된 뒤 적용).
자주 묻는 질문(FAQ)
-
루트 인증서를 서버가 같이 제공해야 하나요?
- 아니요. 클라이언트가 자체 신뢰 저장소에 보유하므로, 일반적으로 루트는 제공하지 않습니다.
-
체인 순서가 중요합니까?
- 네. 서버 인증서 → 중간 인증서(들) 순서가 중요합니다.
-
와일드카드 하나면 모두 해결되나요?
- 아닙니다. *.example.com은 example.com(루트)을 커버하지 않으며, 하위 하위 도메인(a.b.example.com)도 커버하지 않습니다.
-
CloudFront에서 인증서가 있는데도 도메인 불일치가 납니다.
- CloudFront 배포의 Alternate Domain Names(CNAME)에 해당 도메인이 포함되었는지, 그리고 us-east-1 리전의 ACM 인증서를 선택했는지 확인하세요.
문제 해결의 결정 트리(요약)
- 오류 메시지 확인
- authority invalid → 체인 불완전 가능성
- common name invalid / hostname mismatch → 도메인 불일치 가능성
- s_client와 curl로 재현
- -servername, --resolve를 활용해 정확히 어떤 인증서가 제공되는지 확인
- 체인 문제면
- CA 권장 중간 인증서로 fullchain 구성
- 서버/프록시/로드밸런서별로 올바르게 배치
- SSLLabs로 검증
- 도메인 불일치면
- SAN에 필요한 호스트 모두 포함해 재발급
- SNI 기반 매핑/가상호스트 설정 수정
- CDN/ALB의 인증서 연결 및 CNAME 설정 점검
마무리
인증서 체인 불완전과 도메인 불일치는 증상은 비슷해도 해결 경로가 다릅니다. 핵심은 “정확한 재현”과 “올바른 파일/설정 배치”입니다. 이 가이드의 진단 명령과 서버별 설정 예시, 체크리스트를 따라가면 대부분의 문제는 수십 분 내에 해결할 수 있습니다. 이후에는 자동화와 모니터링을 통해 같은 문제가 반복되지 않도록 예방하세요.
안정적으로 신뢰할 수 있는 HTTPS는 사용자 신뢰와 전환율, 검색 노출까지 연결됩니다. 지금 바로 확인하고 정리해 보세요.