Select a display theme:

Custom Node.js 빌드로 지긋지긋한 Zscaler 인증서 문제에서 탈출하기

9 번 조회

보안 솔루션으로 Zscaler를 사용하면, Node.JS 환경에서 HTTPS 요청 시 unable to get local issuer certificate 오류가 발생하는 경우가 있습니다. 보안은 항상 중요하지만, 이 문제가 작업 시 한번씩 튀어나와 은근한 스트레스가 되고 있었습니다. (그 외에도 http 1.1 로 강제 된다던가...)

There was an issue establishing a connection while requesting
(node:73839) UnhandledPromiseRejectionWarning: Error: unable to get local issuer certificate
    at TLSSocket.onConnectSecure (_tls_wrap.js:1058:34)
    at TLSSocket.emit (events.js:198:13)
    at TLSSocket.EventEmitter.emit (domain.js:448:20)
    at TLSSocket._finishInit (_tls_wrap.js:636:8)

피해자 1 피해자 2

문제의 원인

Node.js는 시스템의 CA(인증 기관) 를 사용하지 않고 내부적으로 CA 목록을 node_root_certs.h에 하드코딩하여 사용합니다. 하지만 Zscaler의 인증서는 기본적으로 여기에 포함되어 있지 않기 때문에, HTTPS 요청을 보낼 때 신뢰할 수 없는 인증서로 간주되어 오류가 발생합니다.

근런데 Zscaler의 인증서는 기업마다 다르게 발급되기 때문에, Node.js 공식 CA 목록에 포함되기 어렵습니다. 하지만, 그렇다고 해서 계속 이대로 지낼 수 는 없었습니다.

우린 답을 찾을 것이다. 늘 그랬듯이

Graceful 한 해결방법을 찾아간 과정

일반적으로 다음과 같은 방법으로 문제를 우회할 수 있었습니다.

NODE_TLS_REJECT_UNAUTHORIZED 비활성화 (비추천)

export NODE_TLS_REJECT_UNAUTHORIZED=0

이 방법은 가장 간편한 방법이지만 보안적으로 취약하며, MITM(중간자 공격) 위험이 있습니다. 또한, 일부 경우에 적용되지 않는 경우가 있습니다. 대표적으로 next/font 를 사용하는 경우 build 과정에서 서체 파일을 다운로드 하게 되는데 이 때 위 옵션을 사용 한 경우에도 아래와 같은 오류가 발생합니다.

[Error [FetchError]: request to https://~, reason: unable to get local issuer certificate] {
  type: 'system',
  errno: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
  code: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
  constructor: [Function: FetchError]
}

무엇보다 절대 운영 환경에서 사용하면 안된다고 설명 했음에도 해당 옵션을 운영 환경에서 사용하려는 작업자를 적발하였습니다. 다행히 코드 리뷰 과정에서 발견하였지만, 이러한 처리가 코드에 포함되지 않을 수 있는 다른 방법을 고민하게 되었습니다.

ps: zero trust 의 항상 신뢰할 수 없는 것으로 간주 에 대해 다시 한번 생각해 보게 되었습니다. (팀원도 믿으면 안된다)

NODE_EXTRA_CA_CERTS

export NODE_EXTRA_CA_CERTS=/path/to/zscaler-cert.pem

어느정도 요구사항을 만족 하였지만, 다른 불편함에 봉착 하였습니다.

  • 1개의 파일만 지정할 수 있다. 여러 인증서를 사용 하려면 인증서를 합치는 작업이 필요하다.
  • 인증서를 실수로 삭제하는 경우가 있다.
  • 개발 환경 셋팅한지 오래된 사람의 경우 인증서를 추가 했다는 사실을 잊어서 신규 입사자 온보딩때 헤매는 경우가 있다.
  • 일부 상황에서 여전히 발생한다.

특히, Next.js 에서 개발하는 경우에 오류 상황이 자주 재현 되었습니다.

Next.js proxy failed

이유는 Next.js 는 system 환경 변수 에 NODE_EXTRA_CA_CERTS 가 지정되어 있더라도 런타임에 이를 사용하지 않아 undefined 가 되며 --experimental-https 옵션을 사용할 경우 NODE_EXTRA_CA_CERTS 가 ~/Library/Application Support/mkcert/rootCA.pem 로 변경됩니다. 이 때문에 유독 Next.js 에서 인증서 문제가 지속적으로 발생 하였습니다.

NODE_OPTIONS=--use-openssl-ca

NODE_OPTIONS=--use-openssl-ca

위 방법의 경우에는 openssl 에 포함되어 있으나 Node.js CA 목록에 없는 경우에는 유효 하지만 이번과 같이 Zscaler 처럼 사설 인증서의 경우 해당되지 않습니다.

Node.js에 Zscaler 인증서 추가하여 직접 빌드하기

문득, 이런 생각을 했습니다. Node.js 에 Zscaler 인증서가 없는게 문제라면 추가하면 되는게 아닐까?

그래서 Node.js 코드를 수정해 직접 빌드 해보기로 했습니다.

1. Node.js 소스 코드 다운로드

# Node.js 최신 LTS 버전 다운로드
git clone [email protected]:nodejs/node.git
cd node

2. Zscaler 인증서 추가

Node.js는 src/node_root_certs.h 파일에 기본 CA 목록을 포함하고 있습니다. 여기에 회사의 Zscaler 인증서를 추가합니다. 마지막 줄에는 개행문자가 없고 끝에 쉼표가 있는것에 주의 합니다.

"-----BEGIN CERTIFICATE-----\n"
"(여기에 회사 Zscaler 인증서 내용을 추가)\n"
"-----END CERTIFICATE-----",

3. Node.js 빌드

BUILDING.md 를 참고하여 빌드를 진행합니다.

저는 nvm 을 통해 직접 빌드한 버전을 포함하여 여러 버전을 사용할 계획이기 때문에 추후 쉽게 구분하기 위해서 node_version.h 의 NODE_TAG 의 값을 변경하여 postfix 를 -zscaler 로 수정 하였습니다.

./configure
make -j$(nproc)

여기서 make -j$(nproc) 중 nproc 은 현재 시스템에서 사용 가능한 논리 프로세서 또는 CPU 코어의 개수를 출력하는 명령어 입니다.

mac 에서는 nproc 을 포함하여 일부 리눅스 명령어가 기본 제공되지 않기 떄문에 직접 숫자를 지정 하거나 GUN coreutils 를 설치해 gnproc 으로 대체할 수 잇습니다.

brew install coreutils
./configure
make -j$(gnproc)

# 또는
./configure
make -j4

이제 빌드된 Node.js 를 사용하면, Zscaler 인증서 문제 없이 HTTPS 요청을 정상적으로 처리할 수 있습니다.

# system 에 설치된 Node.js 를 교체 설치 하는 경우
sudo make install

그런데 저는 nvm 으로 관리 및 사내 공유를 위해 다음과 같이 진행 하였습니다.

make install DESTDIR=$(pwd)/node-dist
tar -czvf nodejs-v24.0.0-zscaler.tar.gz -C node-dist usr/local

이제 nodejs-v24.0.0-zscaler.tar.gz 파일을 공유하여 여러 사람이 동일 빌드를 사용할 준비가 되었습니다.

설치하는 쪽 에서는 다음과 같이 진행 합니다.

mkdir -p ~/.nvm/versions/node/v24.0.0-zscaler
tar -xzf ./nodejs-v24.0.0-zscaler.tar.gz -C ~/.nvm/versions/node/v24.0.0-zscaler --strip-components=2
nvm ls

이 때 중요한 점은 공식 배포판과 동일하게 ~/.nvm/versions/node/v24.0.0-zscaler 하위에 bin, lib, include, share 가 존재해야 합니다.

nvm ls 결과

이제 nvm 을 통해 커스텀 빌드 버전과 정식 버전을 옮겨다닐 수 있습니다.


그리고 몇 일 전 feat: added support for reading certificates from macOS system store 이 PR 이 merge 되었습니다. 향후에는 이러한 과정 없이 인증서 문제를 해결 할 수 있기를 기대합니다.