크롬은 되는데 파이어폭스만 SEC_ERROR_UNKNOWN_ISSUER가 나는 이유
“크롬에서는 잘 되는데 파이어폭스에서만 인증서 오류가 납니다.”
인증서 문제 중 자주 접수되고 자주 오진되는 형태다. 브라우저 하나에서 잘 열리니 서버는 멀쩡해 보인다. 하지만 원인은 대개 서버가 체인을 덜 보낸 데 있고, 받는 쪽이 그 빈틈을 채워 주느냐에 따라 결과가 갈린다. 사내 CA를 쓰는 환경이라면 받는 쪽마다 믿는 루트가 다른 것도 원인이 된다.
체인 검증은 클라이언트가 한다
TLS 핸드셰이크에서 서버는 인증서 목록을 보낸다. 보통 이렇게 생겼다.
[0] 서버 인증서 (leaf) CN = example.com
[1] 중간 인증서 CN = YR1, O = Let's Encrypt
[2] 교차 서명 인증서 CN = Root YR, O = ISRG (ISRG Root X1이 서명)
지금 Let’s Encrypt의 기본 체인은 중간 인증서가 두 장이다. 새 루트(Root YR)가 아직 브라우저 트러스트 스토어에 들어가지 않아서, 오래된 루트 ISRG Root X1이 서명해 준 Root YR 인증서를 함께 보낸다.
루트 인증서는 보내지 않는 게 정상이다. 루트는 클라이언트가 이미 신뢰하고 있어야 의미가 있다. 서버가 보내 준 루트를 믿는다면 그건 검증이 아니다.
클라이언트는 서버가 준 leaf에서 출발해 발급자를 따라 올라가며, 자기 트러스트 스토어에 있는 루트에 닿을 때까지 경로를 만든다. 닿으면 신뢰하고, 못 닿으면 오류다.
그런데 트러스트 스토어가 클라이언트마다 다르다.
| 클라이언트 | 트러스트 스토어 |
|---|---|
| Chrome | 자체 Chrome Root Store(105부터 플랫폼별로 전환, iOS 제외) + OS에 직접 추가한 루트 |
| Edge | Microsoft Trusted Root Program 목록(112부터 브라우저에 내장) + OS에 직접 추가한 루트 |
| Safari | 시스템 키체인 |
| Firefox | 자체 스토어. 120부터 Windows, macOS, Android에서는 OS에 추가한 루트도 믿음 |
| curl / openssl (Linux) | /etc/ssl/certs (ca-certificates 패키지) |
| Java 애플리케이션 | JDK의 cacerts |
Firefox는 자체 루트 목록을 쓴다. 120부터는 Windows, macOS, Android에서 사용자나 관리자가 OS에 추가한
루트(그룹 정책으로 배포한 사내 루트 포함)도 기본으로 믿는다. Linux에서는 여전히 가져오지 않는다. 그래서
사내 CA나 TLS 검사 장비를 쓰는 회사의 “크롬만 되는” 현상은 이제 주로 Linux의 Firefox나 120보다 오래된
Firefox에서 나온다. 오래된 버전은 security.enterprise_roots.enabled를 켜고, Linux라면 루트를 Firefox에
직접 넣는다(인증서 관리자나 엔터프라이즈 정책).
중간 인증서가 빠진 서버
가장 흔한 경우다. 서버가 leaf만 보내고 중간 인증서를 빼먹었다.
[0] 서버 인증서 (leaf) CN = example.com
↑ 중간 인증서 없음
그러면 모든 클라이언트가 실패해야 맞는데, 실제로는 크롬과 엣지가 멀쩡히 접속된다. leaf 인증서 안에 발급자를 받아 올 주소가 들어 있어서다.
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -text | grep -A2 "Authority Information Access"
Authority Information Access:
CA Issuers - URI:http://yr1.i.lencr.org/
Let’s Encrypt는 2025-05-07부터 인증서에 OCSP 주소를 넣지 않아서 OCSP - URI 줄이 없다. 다른 CA의 인증서라면 이 줄이 함께
보일 수 있다.
클라이언트가 접속 도중 이 CA Issuers 주소에서 중간 인증서를 직접 받아 오는 동작을 AIA fetching이라고
한다. 데스크톱의 크롬, 엣지, 사파리가 이렇게 한다.
이 주소가 늘 있다고 기대하면 안 된다. BR 2.3.1(SC104, 2026-10-04 시행)부터 서버 인증서의 AIA 확장은 의무(MUST)가 아니라 권고(SHOULD)다. AIA가 빠진 인증서라면 크롬도 중간 인증서를 받아 오지 못한다. 서버가 체인을 다 보내는 것 말고는 답이 없다.
Firefox는 하지 않는다. 핸드셰이크가 느려지고, 사용자가 어떤 사이트에 접속 중인지가 CA에 드러나서 일부러 뺐다. 대신 CA들이 CCADB에 공개한 중간 인증서를 Remote Settings로 미리 내려받아 프로파일에 둔다(데스크톱 Firefox 75, 2020년 4월부터). 그래서 결과만 보면 Firefox도 대체로 통과한다.
증상은 여기서 갈린다.
| 상황 | 크롬, 엣지, 사파리 | Firefox | curl, Java |
|---|---|---|---|
| 공개 CA, 중간 인증서 누락 | 정상 (AIA로 획득) | 대체로 정상 (사전 배포분) | 실패 |
| 사내 CA·사설 CA | 정상 (AIA 가능 시) | 실패 | 실패 |
| 발급 직후 신규 중간 인증서 | 정상 | 실패 가능 (배포 전) | 실패 |
Android 크롬은 AIA를 하지 않아서 중간 인증서가 빠지면 실패한다.
실패할 때 나오는 문구는 클라이언트마다 다르다. Firefox는 SEC_ERROR_UNKNOWN_ISSUER, curl은
unable to get local issuer certificate, 자바는 PKIX path building failed를 낸다.
결국 서버는 처음부터 잘못돼 있었고, 일부 클라이언트가 그걸 덮어 주고 있었던 셈이다. 브라우저에서 잘 열린다고 정상이라 판단하면 API 클라이언트, 모바일 앱, 배치 작업이 조용히 깨진다. 사설 CA를 쓰는 환경이라면 Firefox가 가장 먼저 터진다. 사전 배포 대상이 공개 CA뿐이기 때문이다.
캐시된 중간 인증서
“제 PC에서는 되는데요”가 나오는 이유다.
클라이언트는 한 번 받아 온 중간 인증서를 캐시해 두고 다시 쓴다. 같은 CA를 쓰는 다른 사이트에 들른 적이 있으면 그 중간 인증서가 이미 로컬에 있어서, 서버가 보내 주지 않아도 검증에 성공한다.
그래서 인증서를 자주 다루는 사람의 PC일수록 이 문제를 못 본다. 담당자 PC에서는 정상인데 고객사의 새 PC에서는 오류가 난다면 중간 인증서 누락부터 의심한다.
확인은 캐시가 없는 환경에서 해야 한다. 서버 쪽에서 직접 확인하는 게 가장 확실하다.
# 서버가 실제로 보내는 것만으로 검증되는지
openssl s_client -connect example.com:443 -servername example.com \
-verify_return_error </dev/null 2>&1 | grep -E "Verify return code|verify error"
Verify return code: 0 (ok)
0 (ok)가 아니면 서버가 보내는 체인이 불완전하다는 뜻이다. 브라우저 결과가 어떻든 이쪽이 사실이다.
만료된 교차 서명 루트
체인이 완전한데도 특정 기기에서만 실패하는 경우가 있다. 오래된 안드로이드, 구형 결제 단말, 업데이트가 멈춘 서버가 그렇다.
루트 인증서도 만료된다. 새 루트가 신뢰를 얻기까지 몇 년이 걸려서, CA는 그동안 구 루트로 교차 서명한 중간 인증서를 함께 배포한다. 새 루트를 아는 기기는 짧은 경로로, 모르는 기기는 구 루트를 거치는 긴 경로로 검증할 수 있다.
문제는 구 루트가 만료되는 날 터진다. 2021년 9월 Let’s Encrypt의 DST Root CA X3 만료가 대표적이다. 서버도
인증서도 그대로였는데, 새 루트(ISRG Root X1)를 모르는 채 업데이트가 끊긴 기기와 OpenSSL 1.1.0보다 오래된
라이브러리를 쓰는 클라이언트에서 접속이 끊겼다. 알던 루트는 만료됐고 새 루트는 모르기 때문이다. 정작 많이
걱정하던 구형 안드로이드는 만료된 루트를 계속 믿는 동작 덕에, 이를 노리고 만든 교차 서명으로 버텼다.
서버 설정만으로 완전히 해결되지는 않는다. 어떤 체인을 내보낼지 골라 어느 쪽 기기를 살릴지 정하는 문제에 가깝다.
진단 순서
- 서버가 보내는 체인 전체를 본다.
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
| grep -E "^ [0-9]|subject=|issuer="
0 s:CN = example.com
i:C = US, O = Let's Encrypt, CN = YR1
1 s:C = US, O = Let's Encrypt, CN = YR1
i:C = US, O = ISRG, CN = Root YR
2 s:C = US, O = ISRG, CN = Root YR
i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
각 항목의 i:(issuer)가 바로 다음 항목의 s:(subject)와 일치해야 한다. 끊기는 지점이 빠진 인증서다.
마지막 i:가 공개 루트면 정상이다.
- 순서를 확인한다. leaf가
[0]이어야 하고, 위로 올라가는 순서여야 한다. 순서가 뒤집혀도 관대한 클라이언트는 통과시키지만 엄격한 쪽은 거부한다. - 클라이언트별로 나눠 본다.
# 시스템 트러스트 스토어 기준
curl -sSI https://example.com >/dev/null && echo "curl OK"
# 서버가 보낸 것만으로 검증 (AIA·캐시 배제)
openssl s_client -connect example.com:443 -servername example.com \
-verify_return_error </dev/null >/dev/null 2>&1 && echo "체인 자체 OK"
curl은 되는데 -verify_return_error가 실패하면 로컬 캐시 덕을 보고 있다는 뜻이다.
고치는 법
대부분은 fullchain을 배포하면 끝난다. leaf만 든 파일이 아니라 중간 인증서까지 이어 붙인 파일을 지정한다.
Nginx는 ssl_certificate에 fullchain을 지정한다. ssl_trusted_certificate는 OCSP stapling 검증용이고,
클라이언트에 보내는 체인이 아니다. 헷갈리기 쉽다.
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # leaf + 중간
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
Apache는 2.4.8 이상이면 SSLCertificateFile에 fullchain을 그대로 넣으면 된다. 그 전 버전은
SSLCertificateChainFile로 따로 지정해야 한다.
SSLCertificateFile /etc/ssl/certs/fullchain.pem
SSLCertificateKeyFile /etc/ssl/private/privkey.pem
IIS는 파일이 아니라 저장소로 관리한다. 중간 인증서를 로컬 컴퓨터 → 중간 인증 기관에 설치해야 하고, 여기
없으면 IIS는 체인을 보내지 못한다.
Import-Certificate -FilePath .\intermediate.crt `
-CertStoreLocation Cert:\LocalMachine\CA
교체한 뒤에는 리로드하고, 리로드한 뒤 다시 검증한다. 파일만 바꾸고 리로드를 빼먹으면 디스크는 새 체인, 서빙은 옛 체인이 된다.
교체와 리로드를 실수 없이 하는 절차는 Nginx 인증서 교체와 무중단 리로드에 있다.