NET::ERR_CERT_AUTHORITY_INVALID가 뜰 때
이 글의 순서 9개 절
NET::ERR_CERT_AUTHORITY_INVALID는 원인을 알려 주지 않는다. 화면에는 “연결이 비공개로 설정되어 있지
않습니다”만 뜨고, 그 뒤에 서로 전혀 다른 상황이 네다섯 가지 숨어 있다. 서버 문제일 때도 있고, 그 PC만의
문제일 때도 있고, 회사 보안 장비가 정상 동작한 결과일 때도 있다.
에러가 뜻하는 바는 좁다. 브라우저가 이 인증서에서 출발해 자기가 신뢰하는 루트까지 경로를 만들지 못했다는 것이다. 인증서가 잘못됐다는 뜻은 아니다. “내가 아는 곳에서 나온 인증서인지 확인하지 못했다”는 뜻이다.
비슷한 에러 코드와 구분
Chrome의 인증서 에러는 코드마다 원인 범위가 다르다. 여기서 헷갈리면 엉뚱한 걸 고치게 된다.
| 에러 코드 | 의미 |
|---|---|
ERR_CERT_AUTHORITY_INVALID (-202) |
신뢰 경로를 못 만듦 |
ERR_CERT_COMMON_NAME_INVALID (-200) |
접속한 도메인이 인증서에 없음 (SAN 문제) |
ERR_CERT_DATE_INVALID |
유효기간 밖 (만료 또는 시계) |
ERR_CERT_REVOKED |
폐기된 인증서 |
COMMON_NAME_INVALID는 발급자는 신뢰되는데 도메인이 안 맞는 경우다. 요즘 브라우저는 Common Name을 보지
않고 subjectAltName(SAN)만 본다. CN에 도메인이 있어도 SAN에 없으면 실패한다. 인증서를 다시 발급받아야 하는
문제이고, 신뢰 경로와는 상관이 없다.
아래 내용은 AUTHORITY_INVALID에만 해당한다.
발급자부터 본다
원인마다 처리가 전혀 다르니 발급자부터 확인한다. 브라우저에서는 주소창 → 인증서 정보 →
발급 기관(Issued by)에서 보인다.
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer
issuer 값으로 원인을 가른다.
issuer가 이렇게 나오면 |
원인 |
|---|---|
subject와 동일 |
자기서명 (원인 1) |
사내 CA 이름 (... Internal CA 등) |
루트 미배포 (원인 2) |
| 공개 CA인데 브라우저는 되고 curl·자바·앱만 실패 | 중간 인증서 누락 (원인 3) |
보안 제품 이름 (Fortinet, Zscaler, 백신 등) |
TLS 검사 장비 (원인 4) |
issuer가 예상과 다르면 그 자리에서 답이 나온 것이다. 서버 설정을 뒤지기 전에 여기부터 본다.
자기서명 인증서 (원인 1)
subject와 issuer가 같으면 자기서명이다. 개발 서버, 장비 관리 페이지, 내부 도구에서 흔하다.
subject=CN = dev.internal
issuer=CN = dev.internal ← 동일
브라우저로서는 정상 동작이다. 신뢰할 근거가 없는 인증서를 거부했을 뿐이다.
처리는 상황에 따라 갈린다. 공개 서비스라면 자기서명을 쓰면 안 된다. Let’s Encrypt로 바꾼다. 내부용 도메인이라 HTTP-01 챌린지가 어렵다면 DNS-01로 공인 인증서를 받을 수 있다.
내부 전용이라면 사내 CA를 만들어 그 루트를 배포하는 편이 낫다(원인 2 참고). 장비마다 예외를 클릭하게 두면 진짜 중간자 공격이 왔을 때 아무도 알아채지 못한다.
잠깐 확인만 할 거라면 브라우저에서 예외 처리한다. 다만 고친 게 아니라 미뤄 둔 것이다.
사용자에게 “고급 → 계속 진행”을 안내하고 끝내서는 안 된다. 그게 습관이 되면 경고가 제 기능을 잃는다.
사내 CA 루트 미배포 (원인 2)
issuer가 사내 CA인데 그 PC에 루트가 없는 경우다. 일부 PC만 실패하는 게 특징이다. 배포가 안 된 기기에서만
터진다.
Firefox는 자체 스토어를 쓴다. 120부터는 Windows, macOS, Android에서 OS에 추가한 루트(그룹 정책으로 배포한 사내 루트 포함)도 기본으로 믿지만, Linux의 Firefox와 120보다 오래된 버전은 여전히 모른다.
루트를 트러스트 스토어마다 설치한다.
# Windows — 로컬 컴퓨터의 신뢰할 수 있는 루트
Import-Certificate -FilePath .\corp-root-ca.crt `
-CertStoreLocation Cert:\LocalMachine\Root
# Debian/Ubuntu
sudo cp corp-root-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
# RHEL 계열
sudo cp corp-root-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
오래된 Firefox는 about:config에서 security.enterprise_roots.enabled를 true로 바꾸면 OS
스토어도 같이 참조한다. 이 설정은 정책 파일(policies.json)로도 배포할 수 있다. 아니면 엔터프라이즈 정책의
Certificates.Install로 루트를 직접 배포한다.
서버와 컨테이너도 잊지 않는다. 사람이 쓰는 PC만 챙기고 배치 작업이나 컨테이너를 빠뜨리면 새벽에 도는 잡이 조용히 실패한다. 컨테이너 이미지는 빌드 단계에서 루트를 넣어야 한다.
중간 인증서 누락 (원인 3)
issuer가 정상적인 공개 CA인데도 실패하는 경우다. 서버가 중간 인증서를 보내지 않아 경로가 끊겼다.
특징이 뚜렷하다. 크롬은 되는데 curl, 자바, 모바일 앱이 실패한다. 크롬은 AIA fetching으로 빠진 인증서를 몰래 채워 넣기 때문이다.
# 서버가 보낸 것만으로 검증되는가
openssl s_client -connect example.com:443 -servername example.com \
-verify_return_error </dev/null 2>&1 | grep "Verify return code"
Verify return code: 0 (ok)가 아니면 서버 체인이 불완전하다. fullchain을 배포하면 된다. 판별과 수정 절차는
인증서 체인 오류가 브라우저마다 다르게 보이는 이유에 자세히
적었다.
TLS 검사 장비 (원인 4)
가장 오진하기 쉬운 경우다. issuer에 보안 제품 이름이 나오면 여기다.
issuer=CN = FortiGate CA, O = Fortinet
issuer=CN = Zscaler Intermediate Root CA
issuer=CN = AhnLab ...
방화벽, 프록시, 백신이 HTTPS 트래픽을 검사하려고 연결을 끊고 자기 인증서로 다시 맺은 것이다. 구조로는 중간자 공격과 같다. 그래서 그 장비의 루트가 설치돼 있지 않으면 브라우저가 정확히 이 에러를 낸다.
서버는 아무 잘못이 없다. 이 상태에서 서버 인증서를 재발급하면 시간만 버린다.
판별은 쉽다. 서버에서 본 인증서와 클라이언트에서 본 인증서가 다르다.
# 서버에서 (검사 장비를 안 거침)
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -fingerprint -sha256
문제가 나는 PC의 브라우저에서 본 지문이 이 값과 다르면 중간에 누가 끼어든 것이다.
대응은 이렇게 나뉜다.
- 사내 정책상 정상 동작이면 그 장비의 루트 CA를 클라이언트에 배포한다(원인 2와 같다)
- 특정 서비스는 검사하면 안 된다면 장비에서 그 도메인을 검사 예외(bypass)로 등록한다
- 개인 PC의 백신이 원인이면 그 제품의 HTTPS 스캐닝 설정을 확인한다
- 인증서 피닝을 쓰는 앱은 예외 등록 말고는 방법이 없다. 루트를 설치해도 실패한다
시스템 시계 (원인 5)
드물지만 고치기는 제일 쉽다. 시계가 크게 어긋나면 루트 인증서가 아직 유효하지 않거나 이미 만료된 것으로
판정된다. 보통은 ERR_CERT_DATE_INVALID로 나오지만, 경로를 만드는 단계에서 걸리면 AUTHORITY_INVALID로도
뜬다.
CMOS 배터리가 죽은 PC, 초기화된 VM, 네트워크가 격리돼 NTP를 못 받는 서버에서 나온다.
timedatectl status # Linux
w32tm /query /status # Windows
sudo sntp -sS time.apple.com # macOS 강제 동기화
날짜가 몇 년 단위로 틀어져 있으면 이게 원인이다. 인증서를 건드리기 전에 시계부터 맞춘다.
루트가 만료되거나 제거됐을 때 (원인 6)
서버도 정상이고 체인도 완전하고 시계도 맞는데 오래된 기기에서만 실패하는 경우다.
트러스트 스토어 업데이트가 멈춘 기기는 새 루트를 모른다. 알던 루트는 만료됐다. 2021년 9월 Let’s Encrypt
DST Root CA X3 만료 때 업데이트가 끊긴 오래된 기기와 OpenSSL 1.1.0보다 오래된 클라이언트가 끊긴 게 이
경우다(구형 안드로이드는 별도 교차 서명으로 버텼다).
CA가 대체 체인을 준다면 서버에서 내보낼 체인을 골라 구형 기기를 살릴 수 있는지 본다. 안 되면 그 기기들을 지원 대상에서 뺄지 정해야 한다. 서버 설정으로 해결되는 문제가 아니다.
판별 순서
1. issuer 확인
└ subject와 같음 → 자기서명 (원인 1)
└ 사내 CA → 루트 배포 (원인 2)
└ 보안 제품 이름 → TLS 검사 장비 (원인 4)
└ 정상 공개 CA → 2번으로
2. openssl -verify_return_error
└ 0 (ok) 아님 → 중간 인증서 누락 (원인 3)
└ 0 (ok) → 3번으로
3. 실패하는 기기의 범위
└ 특정 PC만 → 트러스트 스토어 / 시계 (원인 2, 5)
└ 오래된 기기만 → 루트 만료 (원인 6)
서버 인증서 재발급은 이 순서 어디에도 없다. AUTHORITY_INVALID의 원인은 대부분 서버가 아니라 검증하는 쪽에
있다. 재발급부터 하고 보는 습관이 가장 흔한 시간 낭비다.
원인이 중간 인증서 누락으로 좁혀졌다면 고치는 절차는 Nginx 인증서 교체와 무중단 리로드에 있다.