OpenSSL 문제 해결: unable to get local issuer certificate
오류 해결하기
PHP 환경에서의 네트워크 요청 실행, openssl
혹은 curl
명령 실행, 또는 Node.js/Python에서 SSL/TLS 연결을 생성하는 과정에서, 개발자는 20:unable to get local issuer certificate
라는 오류 메시지를 종종 접할 수 있습니다. 이 오류는 OpenSSL이 피어 인증서를 검증하는 과정에서 발생하는 대표적인 문제입니다.
보안을 위해, OpenSSL은 기본적으로 신뢰할 수 있는 루트 인증 기관(CA)을 명확히 알고 있어야만 인증서 체인을 검증할 수 있습니다. 이러한 신뢰 지점이 누락되었거나 명시적으로 지정되어 있지 않으면, OpenSSL은 서버 인증서의 유효성을 확인할 수 없어 해당 오류를 발생시킵니다.
이 문서에서는 해당 오류의 원인을 자세히 설명하고, ServBay 환경에서 PHP, Python, Node.js, curl
명령 등 다양한 환경에 맞춰 OpenSSL의 신뢰 저장소를 올바르게 설정하는 방법을 안내합니다.
오류 메시지: 20:unable to get local issuer certificate
문제 설명
OpenSSL이 원격 서버의 SSL/TLS 인증서를 검증하려 할 때, 서버 인증서에서 신뢰할 수 있는 루트 CA까지의 인증서 체인을 생성합니다. 만약 이 체인을 구성하는 중간 인증서나 루트 CA 인증서를 로컬에서 찾지 못하거나, 인증서를 탐색할 준비가 된 신뢰 저장소(CAFile 또는 CAPath)를 찾을 수 없으면 검증은 실패하며 20:unable to get local issuer certificate
오류가 반환됩니다.
즉, OpenSSL이 어느 인증서 기관을 신뢰해야 하는지 알지 못해, 접속 중인 서버의 신원을 확인할 수 없다는 뜻입니다.
ServBay의 OpenSSL 버전 및 CA 인증서 경로
ServBay는 통합 로컬 웹개발 환경으로, OpenSSL 패키지와 함께 일반적으로 사용되는 공개 루트 CA 인증서를 사전 탑재하여 제공합니다. ServBay에서 사용하는 OpenSSL 버전은 사용자의 칩셋에 따라 다릅니다.
- Apple Silicon(M 시리즈 칩)용 ServBay: OpenSSL 3.2.1 버전 사용
- Intel 칩셋용 ServBay: OpenSSL 1.1.1u 버전 사용
각 버전의 OpenSSL에 대응하는 CA 인증서 파일(cacert.pem
)과 인증서 디렉토리(certs
)는 ServBay 소프트웨어 패키지 설치 경로 아래에 위치합니다. 사용 중인 ServBay 버전에 맞는 경로를 찾아야 합니다.
ini
# 신뢰할 수 있는 모든 루트 인증서가 포함된 번들 파일
cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
# 개별 인증서 파일이 저장된 디렉토리 (일반적으로 cacert.pem만으로 충분하나, 일부 애플리케이션에서는 capath 필요)
capath=/Applications/ServBay/package/common/openssl/3.2/certs
1
2
3
4
2
3
4
ini
# 신뢰할 수 있는 모든 루트 인증서가 포함된 번들 파일
cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
# 개별 인증서 파일이 저장된 디렉토리
capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs
1
2
3
4
2
3
4
unable to get local issuer certificate
오류 해결의 핵심은 OpenSSL이 신뢰할 수 있는 CA 인증서를 찾을 수 있도록 위의 cafile
또는 capath
경로를 명확히 지정해주는 것입니다.
해결책 예시
아래는 OpenSSL CA 신뢰 저장소 경로를 다양한 도구나 언어 환경에서 지정하는 방법 예시입니다.
예시 1: openssl
명령어로 연결 테스트
openssl s_client
명령어로 연결 테스트를 하는 중 오류가 발생했다면:
bash
openssl s_client -quiet -connect gmail.com:443
1
아래와 같이 verify error:num=20:unable to get local issuer certificate
메시지가 포함된 오류 출력을 볼 수 있습니다.
bash
depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R1
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WR2
verify return:1
depth=0 CN=gmail.com
verify return:1
# ... 기타 연결 정보 ...
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
해결 방법:
-CAfile
옵션에 ServBay의 CA 인증서 번들 파일 경로를 명확히 지정합니다.
bash
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/3.2/cacert.pem
1
bash
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
1
정상적으로 연결되고 인증서가 검증되면 출력값의 verify return
값은 모두 1
이며, 더 이상 verify error:num=20
줄이 나타나지 않습니다.
bash
depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R1
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WR2
verify return:1
depth=0 CN=gmail.com
verify return:1
# ... 기타 연결 정보 ...
1
2
3
4
5
6
7
2
3
4
5
6
7
예시 2: PHP에서 OpenSSL 사용
PHP의 여러 네트워크 기능들(예: HTTPS URL 접근을 위한 file_get_contents
, SSL 연결 생성용 stream_socket_client
, cURL 확장 등)은 내부적으로 OpenSSL을 활용합니다. php.ini
수정 또는 stream context 옵션 설정을 통해 신뢰 저장소를 지정할 수 있습니다.
방법 A: php.ini
수정 (권장)
가장 간편한 전역 적용 방법입니다. 사용 중인 PHP 버전의 php.ini
파일(ServBay 컨트롤 패널에서 편집 가능) 내 [openssl]
섹션에 아래 내용을 추가 또는 수정합니다. 칩셋에 따라 알맞은 경로를 사용해야 합니다.
ini
[openssl]
; 신뢰할 수 있는 CA 인증서 번들 파일 지정
openssl.cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
; CA 인증서 디렉토리 지정 (선택, 권장)
openssl.capath=/Applications/ServBay/package/common/openssl/3.2/certs
1
2
3
4
5
2
3
4
5
ini
[openssl]
; 신뢰할 수 있는 CA 인증서 번들 파일 지정
openssl.cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
; CA 인증서 디렉토리 지정 (선택, 권장)
openssl.capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs
1
2
3
4
5
2
3
4
5
설정 후에는 ServBay의 PHP 서비스(또는 전체 ServBay)를 재시작해야 변경 사항이 적용됩니다.
방법 B: 코드 내 구성 옵션 추가 (현재 연결에만 반영됨)
전역 php.ini
를 수정하지 않으려면, stream_context_create
를 활용한 SSL/TLS 연결 컨텍스트 설정 시 ssl
옵션에 cafile
을 지정할 수 있습니다.
php
<?php
// 예시: SSL/TLS를 사용하는 SMTP 서버 연결
$server = 'ssl0.ovh.net';
$port = 465;
// 사용 중인 ServBay 버전에 따라 적합한 CA 파일 경로 설정
// Apple Silicon의 경우:
$caCertFile = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem';
// Intel의 경우:
// $caCertFile = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem';
$contextOptions = [
'ssl' => [
'verify_peer' => true, // 피어 인증서 검증 활성화
'verify_peer_name' => true, // 연결된 호스트명이 인증서에 맞는지 검증
'allow_self_signed' => false, // 신뢰되지 않은(자체 서명) 인증서 허용 안 함
'cafile' => $caCertFile, // CA 인증서 번들 파일 지정
// 'capath' => '/Applications/ServBay/package/common/openssl/3.2/certs', // 선택 사항, CA 인증서 폴더 지정
],
];
$context = stream_context_create($contextOptions);
// 정의된 컨텍스트로 SSL/TLS 연결 생성
$connection = @stream_socket_client(
"ssl://$server:$port",
$errno,
$errstr,
30, // 연결 타임아웃
STREAM_CLIENT_CONNECT,
$context // 컨텍스트 옵션 전달
);
if ($connection) {
echo "Connection established to $server:$port\n";
// 예시: EHLO 명령 송신
fwrite($connection, "EHLO servbay.demo\r\n"); // ServBay 브랜드의 예시 도메인 사용
while (!feof($connection)) {
echo fgets($connection);
}
fclose($connection);
} else {
echo "Failed to connect to $server:$port. Error: $errstr ($errno)\n";
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
예시 3: Python에서 OpenSSL 사용 (ssl 모듈)
Python의 ssl
모듈에서도 SSL/TLS 컨텍스트 생성 시 신뢰할 CA 인증서를 지정할 수 있습니다.
python
import ssl
import socket
server = 'ssl0.ovh.net'
port = 465
# ServBay 버전에 따른 적합한 CA 파일 경로 선택
# Apple Silicon:
ca_cert_file = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem'
# Intel:
# ca_cert_file = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem'
# 기본 SSL 컨텍스트에 CA 파일 지정
context = ssl.create_default_context(cafile=ca_cert_file)
# 디렉토리 지정 예시: context = ssl.create_default_context(capath='/Applications/ServBay/package/common/openssl/3.2/certs')
try:
# 일반 socket 연결 생성
with socket.create_connection((server, port)) as sock:
# socket을 SSL socket으로 래핑
with context.wrap_socket(sock, server_hostname=server) as ssock:
print(f"SSL connection established. Negotiated Protocol: {ssock.version()}")
# EHLO 명령 전송 예시
ssock.sendall(b"EHLO servbay.demo\r\n") # ServBay 예시 도메인 사용
while True:
data = ssock.recv(4096)
if not data:
break
print(data.decode())
except Exception as e:
print(f"Failed to connect or SSL error: {e}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
예시 4: Node.js에서 OpenSSL 사용 (tls 모듈)
Node.js의 tls
모듈로 TLS/SSL 연결 시, 연결 옵션의 ca
속성에 신뢰할 CA 인증서를 지정할 수 있습니다. ca
는 하나 이상의 인증서 내용을 담은 문자열 또는 Buffer 배열이 될 수 있습니다. ServBay의 cacert.pem
파일 내용을 읽어 사용하는 것이 가장 간편합니다.
javascript
const tls = require('tls');
const fs = require('fs');
const server = 'www.google.com'; // 신뢰할 수 있는 표준 웹사이트 사용 예시
const port = 443;
// ServBay 버전에 따라 알맞은 CA 파일 경로 지정
// Apple Silicon:
const caCertFile = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem';
// Intel:
// const caCertFile = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem';
const options = {
host: server,
port: port,
// CA 인증서 파일 내용 읽어오기
ca: fs.readFileSync(caCertFile),
// Node.js tls 모듈은 기본적으로 서버 호스트명 검증(checkServerIdentity)을 수행
// CA 파일이 올바르게 지정되고, 서버 인증서가 유효하다면 검증에 성공함
// 특별한 사유, 보안 위험성을 정확히 인지하지 않는 한 checkServerIdentity를 비활성화하지 마세요
// checkServerIdentity: () => { return null; } // <-- 이 옵션은 중요한 보안 검사를 비활성화하므로 사용 금지!
};
const socket = tls.connect(options, () => {
console.log('SSL connection established');
// HTTPS 연결의 경우 HTTP 요청을 추가적으로 보내야 하지만, 여기서는 연결 테스트만 예시함
// socket.write('GET / HTTP/1.1\r\nHost: ' + server + '\r\n\r\n');
});
socket.on('data', (data) => {
console.log(data.toString());
});
socket.on('close', () => {
console.log('Connection closed');
});
socket.on('error', (error) => {
console.error('Error:', error.message); // 오류 메시지 출력
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
참고: Node.js 예시에서는 checkServerIdentity: () => { return null; }
옵션을 제거했습니다. 이 옵션은 서버 호스트명 검증을 우회하여, 심각한 보안 취약점이 발생할 수 있습니다. OpenSSL의 unable to get local issuer certificate
오류는 루트 신뢰와 관련된 문제이며, 호스트명 검증(verify_peer_name
)과는 별개입니다. ca
옵션을 정확하게 지정하고 서버 인증서에 문제가 없다면, Node.js의 기본 호스트명 검증은 정상적으로 성공합니다. 만약 호스트명 검증 오류가 발생한다면 일반적으로 인증서 자체 문제이지, CA 신뢰 저장소 문제는 아닙니다.
예시 5: curl
명령에서 OpenSSL 사용
curl
도 HTTPS 요청시에 OpenSSL(또는 기타 SSL 라이브러리)을 사용합니다. --cacert
옵션을 사용하면 CA 인증서 번들 파일을 직접 지정할 수 있습니다.
bash
# ServBay의 CA 인증서 파일로 HTTPS 사이트에 접근 예시
# 사용 중인 ServBay 버전에 적합한 경로 사용
# Apple Silicon:
curl --cacert /Applications/ServBay/package/common/openssl/3.2/cacert.pem https://example.com
# Intel의 경우:
# curl --cacert /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem https://example.com
1
2
3
4
5
6
7
2
3
4
5
6
7
CA 인증서 경로가 올바르며 서버 인증서에 문제가 없다면, curl
은 검증 오류 없이 정상적으로 콘텐츠를 가져올 수 있습니다.
요약
OpenSSL 기반 SSL/TLS 연결 사용 시 발생하는 20:unable to get local issuer certificate
오류는 매우 흔하며, 그 근본 원인은 OpenSSL이 서버 인증서를 신뢰할 저장소 경로를 명확히 지정해줘야 하기 때문입니다. ServBay는 개발자에게 일반적으로 사용되는 공용 루트 CA 인증서를 미리 번들로 제공하여, 추가 준비 없이 사용할 수 있도록 지원합니다.
이 문제를 해결하려면, 개발 환경(php.ini, 코드 내 SSL 컨텍스트 옵션, openssl
명령의 -CAfile
, curl
의 --cacert
등)에서 ServBay가 제공하는 cacert.pem
파일 경로를 정확히 지정해야 합니다. 반드시 자신의 macOS 칩셋(Apple Silicon 또는 Intel)과 그에 맞는 ServBay OpenSSL 버전에 해당하는 경로를 사용해야 합니다. CA 신뢰 저장소를 올바르게 구성하면, 로컬 개발 환경에서도 외부 SSL/TLS 서비스와 안정적으로 안전하게 통신할 수 있습니다.