TechFeedTechFeed
Backend

UNABLE_TO_VERIFY_LEAF_SIGNATURE, 자체 서명 인증서, NODE_TLS | HTTPS만 실패하면?

UNABLE_TO_VERIFY_LEAF_SIGNATURE는 코드가 틀린 게 아니라 노드가 HTTPS 리프 서명을 신뢰 저장소로 검증하지 못해 핸드셰이크를 끊은 줄입니다. 중간서, 회사 프록시, mkcert, NODE_EXTRA_CA_CERTS. Node.js, Next.js, 한국 1인 개발자 기준. 2026년 9월 Node.js TLS·CLI 문서.

by

UNABLE_TO_VERIFY_LEAF_SIGNATURE는 코드가 틀린 게 아니라, 노드가 HTTPS 상대의 인증서 체인을 따라가다 리프 서명을 신뢰 저장소로 검증하지 못해 핸드셰이크를 끊은 줄입니다. HTTP는 되고 HTTPS만 실패하면 인증서부터 보세요.


로컬 mkcert나 회사 프록시, 카카오 API를 붙이다 이 줄이 뜨는 일, 한국에서 넥스트를 혼자 돌리면 한 번은 만납니다. 거절·리셋과 달리 소켓은 붙었고, 검증만 실패한 겁니다.


NODE_TLS_REJECT_UNAUTHORIZED를 0으로 두기 전에 체인과 중간 인증서부터 맞추세요. 근거는 Node.js TLSNODE_TLS_REJECT_UNAUTHORIZED에 있습니다.


리프 서명을 못 검증한다는 뜻

상대가 준 맨 앞 인증서 서명을 노드가 믿지 못해서 납니다. 브라우저는 운영체제 저장소를 쓰고, 노드는 자신이 묶은 루트 목록을 씁니다. 브라우저로 같은 주소가 초록인데 fetch만 실패하는 이유가 여기 있습니다.


리프는 서버 이름 인증서입니다. 그 아래 중간 인증서가 빠지면 루트까지 사다리가 끊깁니다. 회사 SSL 검사 프록시는 자체 루트를 끼워 넣는데, 크롬에는 그 루트가 있고 노드에는 없습니다. 한국 사무실망에서 카카오·토스 엔드포인트만 실패하는 그림이 자주 나옵니다.


자체 서명 로컬 HTTPS도 같습니다. mkcert로 만든 인증서를 브라우저에는 넣었는데 노드 프로세스에는 안 넣으면, next dev --experimental-https 앞의 API 호출이 이 줄로 죽습니다. 포트가 거절된 줄은 ECONNREFUSED, 붙었다가 잘린 줄은 ECONNRESET입니다.


0으로 끄지 말 것 | NODE_TLS_REJECT_UNAUTHORIZED=0은 검증을 전부 끕니다. 로컬 한 줄 우회가 프리뷰까지 따라가면 중간자 검사를 못 합니다. 원인 체인을 고치는 쪽이 안전합니다.


브라우저 저장소와 노드 루트 목록이 달라 HTTPS 검증이 갈리는 개념 이미지
브라우저는 되고 노드 fetch만 실패하면 루트 저장소가 다른 것이다

먼저 가릴 세 칸

빠진 중간서, 회사 프록시, 로컬 자체 서명 세 칸이면 원인이 갈립니다.


증상먼저 할 일
중간 인증서 누락일부 API만 실패, openssl은 경고서버가 체인 전체를 보내는지 확인
회사 SSL 검사사무실망만 실패, 테더링은 통과회사 루트를 NODE_EXTRA_CA_CERTS로
로컬 자체 서명localhost HTTPS만 실패mkcert 루트를 노드에 연결
만료·이름 불일치다른 코드(CERT_HAS_EXPIRED 등)이 글이 아니라 그 코드 문서
환경 변수 우회로컬만 통과, CI는 실패0 플래그가 로컬 셸에만 있는지

네 번째 칸은 코드가 다릅니다. 만료는 CERT_HAS_EXPIRED, 이름 불일치는 ERR_TLS_CERT_ALTNAME_INVALID입니다. 리프 서명 실패만 이 글에서 봅니다. 호스트 이름을 못 푸는 줄은 ENOTFOUND입니다.


상대가 보내는 체인 확인
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null # 출력에서 인증서가 1장만이면 중간서가 빠진 경우가 많다 # Verify return code 가 0이 아니면 원인 숫자가 함께 찍힌다

추가 인증서는 끄기가 아니다

회사 루트나 mkcert 루트는 노드에 파일로 넘깁니다. NODE_EXTRA_CA_CERTS는 기본 저장소에 파일을 더할 뿐, 검증을 끄지 않습니다. 공식 CLI 문서도 이 변수를 추가 신뢰 목록으로 설명합니다.


PEM 여러 장을 한 파일에 이어 붙여도 됩니다. 맥 키체인에만 넣고 이 변수를 비우면 브라우저는 되고 크론·깃허브 액션은 실패합니다. 한국에서 launchd로 발행 스크립트를 돌리는 1인이라면, 셸 프로필에만 변수가 있고 plist에는 없는 사고를 자주 봅니다.


fetch에 ca 옵션을 직접 주는 길은 언디치 에이전트를 쓸 때입니다. 기본 fetch는 환경 변수가 더 단순합니다. 환경 변수 칸이 빌드와 런타임에서 갈리는 문제는 NEXT_PUBLIC 글과 자리가 다릅니다. 여기는 TLS 검증용 파일 경로만 봅니다.


추가 CA만 넣고 검증은 유지
# 회사 루트 또는 mkcert 루트 PEM export NODE_EXTRA_CA_CERTS="$HOME/.local/share/mkcert/rootCA.pem" node -e "fetch('https://localhost:3000/api/health').then(r=>r.text()).then(console.log)" # 금지에 가까운 우회 (저장소·CI에 넣지 말 것) # export NODE_TLS_REJECT_UNAUTHORIZED=0
  • [ ] openssl로 체인이 몇 장인지 봤다
  • [ ] 사무실망과 테더링을 비교했다
  • [ ] NODE_EXTRA_CA_CERTS에 PEM 경로를 넣었다
  • [ ] 0 플래그를 저장소와 CI에서 뺐다
  • [ ] 크론·액션 환경에도 같은 변수가 있는지 봤다

NODE_EXTRA_CA_CERTS로 추가 루트를 노드에 연결하는 개념 이미지
추가 인증서 변수는 검증을 끄지 않고 루트만 더한다

로컬 HTTPS와 프리뷰가 갈릴 때

로컬만 실패하면 자체 서명이 원인인 경우가 많습니다. 프리뷰만 실패하면 서버가 중간서를 안 붙였거나, 버셀 앞 인증서와 오리진 인증서가 다릅니다. 함수가 오리진을 HTTPS로 다시 부를 때 노드 검증이 한 번 더 돕니다.


카카오 로그인 리다이렉트가 HTTPS인데 로컬 콜백이 HTTP면, 브라우저 혼합 콘텐츠와 TLS 검증이 겹칩니다. 콜백 URL은 콘솔에 등록한 값과 한 글자도 같아야 하고, 로컬에서 HTTPS를 쓰려면 mkcert 루트를 노드에도 넣어야 합니다.


시간 초과로 보이는 경우도 있습니다. 핸드셰이크가 오래 실패하면 겉에 타임아웃이 남고 안쪽에 검증 실패가 있습니다. ETIMEDOUT만 보고 한도를 늘리면 인증서 문제가 숨습니다. cause 코드를 같이 찍으세요.


코드무엇을 못 했나우회
UNABLE_TO_VERIFY_LEAF_SIGNATURE리프 서명 사다리체인·추가 CA
SELF_SIGNED_CERT_IN_CHAIN체인에 자체 서명그 루트를 추가 CA로
CERT_HAS_EXPIRED유효 기간재발급, 시계
ERR_TLS_CERT_ALTNAME_INVALID이름과 호스트 불일치SAN에 그 호스트

시계도 원인이다 | 컨테이너나 오래된 가상 머신의 날짜가 이전이면 유효한 인증서도 만료로 보이거나 서명이 이상해 보입니다. 검증 실패가 이상하면 date부터 보세요.


실전에서 고치는 순서

브라우저와 노드를 같은 주소로 비교합니다. 브라우저만 되면 저장소 차이입니다. openssl로 체인 장수를 보고, 1장이면 서버 설정을 고칩니다. 회사망만이면 루트 PEM을 받아 추가 CA로 넣습니다.


그래도 안 되면 코드가 다른지 확인합니다. 리셋·거절·DNS가 섞여 있으면 TLS가 아닙니다. CORS는 브라우저가 읽고, 이 줄은 서버 사이드 fetch에서 납니다. 헤더 허용은 CORS 글입니다.


마지막에야 로컬 한시 우회를 씁니다. 셸에서만 0을 켜고, 커밋하지 않습니다. CI와 프리뷰에는 절대 넣지 마세요. 폴더별 환경은 direnv로 나누면 실수가 줄어듭니다.


  • [ ] 브라우저와 노드 fetch를 같은 URL로 비교했다
  • [ ] openssl -showcerts로 체인 장수를 봤다
  • [ ] 추가 CA 변수를 크론까지 맞췄다
  • [ ] 0 플래그를 저장소에서 검색해 지웠다
  • [ ] 코드가 리셋·거절이 아님을 확인했다

로컬 mkcert HTTPS와 프리뷰 인증서 체인을 비교하는 개념 이미지
로컬 자체 서명과 프리뷰 중간서 누락은 증상은 같고 고치는 칸이 다르다

참고 자료


내부 연계: 소켓 리셋, 연결 거절, DNS, CORS, 폴더 환경 변수


인용한 동작은 2026년 9월 공개 문서 기준입니다.


자주 묻는 질문

크롬에선 되는데 노드만 실패합니다. 서버 인증서가 잘못된 건가요?

서버가 틀린 경우도 있고, 노드 저장소에 중간서·회사 루트가 없는 경우도 있습니다. openssl로 체인 장수를 보고, 1장이면 서버, 장수는 맞는데 노드만 실패면 추가 CA입니다.


NODE_TLS_REJECT_UNAUTHORIZED=0을 넣으면 해결되나요?

검증이 꺼져서 통과처럼 보입니다. 중간자 검사를 못 하고, 변수가 CI에 남으면 사고입니다. 로컬 한 줄 실험 말고는 추가 CA로 고치세요.


사무실에서만 카카오 API가 실패합니다.

SSL 검사 프록시가 자체 루트를 끼운 그림입니다. 테더링으로 통과되면 회사 PEM을 NODE_EXTRA_CA_CERTS에 넣습니다. 카카오 콘솔 키 문제가 아닙니다.


mkcert로 만들었는데도 스크립트가 실패합니다.

브라우저·키체인에만 루트가 들어간 상태입니다. 노드 프로세스는 그 저장소를 안 봅니다. mkcert 루트 PEM 경로를 추가 CA 변수에 넣어야 스크립트도 통과합니다.


SELF_SIGNED_CERT_IN_CHAIN과 같은 줄인가요?

가깝지만 코드가 다릅니다. 체인에 자체 서명이 보이면 SELF_SIGNED, 리프 서명 사다리가 끊기면 UNABLE_TO_VERIFY_LEAF_SIGNATURE입니다. 고치는 법은 둘 다 그 루트를 신뢰 목록에 넣는 쪽입니다.


프리뷰 배포만 이 줄이 납니다.

오리진 서버가 중간서를 안 붙였거나, 함수가 사설 HTTPS를 다시 부르는 경우입니다. 로컬 0 플래그가 프리뷰에는 없어서 드러나기도 합니다. 배포 환경에서 openssl로 체인을 다시 보세요.


리프 서명 실패는 노드가 그 체인을 믿을 루트가 없는 줄입니다. 검증을 끄지 말고 체인 장수와 추가 CA를 맞추세요. 관련 글: 소켓 리셋, DNS, 폴더 환경.


UNABLE_TO_VERIFY_LEAF_SIGNATURE자체 서명 인증서NODE_TLSHTTPSmkcert넥스트노드백엔드TLS개발자

함께 보면 좋은 문제 해결

EXPLORE / Backend

이어서 읽어보기

전체 토픽 둘러보기