python을 하다보면 인터넷망의 ssl 인증서를 변조해서 운영하는 회사를 만나게 된다. 이런 경우는 naver에 접속을 해도 naver인증서가 아닌 자사의 인증서로 변조되어 운영한다. 내부적으로는 보안담당자가 내부 정책에 의해 암호화된 패킷을 확인하려는 목적으로 이런 환경이 구성된다. 회사 내부의 보안을 위해서 흔히 그렇게 하는 장비가 제공된다.
그런데 이런 환경하에서 pip install을 실행하면 억울하게도 아래와 같은 오류를 만나게 된다. python에게는 해당 변조된 인증서가 신뢰받는 인증서가 아니기 때문에 인증서 검증이 실패하는 것이다. 일반 인터넷 환경에서는 만날 일이 없는 오류이다.
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate...
(참고로, 위에서 CERTIFICATE_VERIFY_FAILED 대신에 UNSAFE_LEGACY_RENEGOTIATION_DISABLED 를 만난다면 https://finai.tistory.com/22 를 추가로 참조할 수 있다.)
이 경우는 대체 어떻게 해결을 할 수 있을까? 결론부터 이야기하면 신뢰받지 않은 이 인증서를 python에서 강제로 받아들이도록 설정하면 된다(정확히는 python의 requests 패키지가 사용하는 인증서이다). 윈도우에서도 유효하지만, 여기서는 mac os를 가정해보자.
맨 먼저 이 현상을 자세히 설명할 수 있는 간단한 코드를 짜보자.
$ cat > check_badssl.py
import requests
data = {'foo':'bar'}
url = 'https://self-signed.badssl.com'
r = requests.post(url, data=data)
위 사이트는 인터넷망에서 공개된, 신뢰받지 않은 인증서를 갖는 사이트이다. 실행해보면 이런 오류를 볼 수 있다.
$ python check_badssl.py
Traceback (most recent call last):
....
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1123)
이제 우리는 신뢰받지 않는 인증서를 가진 사이트에 접속하면 생기는 오류를 언제든지 재현해낼 수 있게 되었다(위 python script의 사이트 명만 바꾸면 다른 사이트 대상으로도 가능하다. 이를테면 인증서 변도된 https://pypi.org도 가능하다).
그러면 이제 python에서 어떤 인증서들이 신뢰받는 인증서로 미리 입력되어 있는지 확인해보자. certifi라는 패키지를 통해서 가능하다. 이 패키지가 없다면 우회 다운로드 해서라도 설치해야 하는데, 패키지를 우회해서 받는 방법은 폐쇄망에서의 python package설치에 대해 다룬 이 블로그의 다른 글을 참조한다. 여기서는 certifi 패키지가 설치되어 있다고 가정했다.
$ cat > check_certifi.py
import certifi
from cryptography import x509
from cryptography.hazmat.backends import default_backend
list_of_cert = certifi.contents().split("\n\n")
print(certifi.where())
#print(list_of_cert)
for cert in list_of_cert:
details = x509.load_pem_x509_certificate(cert.encode('utf-8'), default_backend())
print (details.issuer, details.not_valid_after)
$ python check_certifi.py | more
/Users/id/miniforge3/lib/python3.9/site-packages/certifi/cacert.pem
....
<Name(C=BE,O=GlobalSign nv-sa,OU=Root CA,CN=GlobalSign Root CA)> 2028-01-28 12:00:00
<Name(CN=kbankwith.com)> 2026-09-11 02:49:40
<Name(OU=GlobalSign Root CA - R2,O=GlobalSign,CN=GlobalSign)> 2021-12-15 08:00:00
<Name(O=Entrust.net,OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.),OU=(c) 1999 Entrust.net Limited,CN=Entrust.net
....
위 첫 줄의 cacert.pem파일 경로가 바로 이 신뢰받는 인증서의 근거가 되는 인증서 정보를 모아놓은 파일이다(pem은 base64라는 방식으로 인코딩된 인증서들을 가지고 있다)
그러면 우리가 접속해야할 사이트의 인증서를 확보해보자. 아래 openssl 명령으로 접속을 원하는 사이트를 입력해보면 pem 형식의 인증서를 확보할 수 있다. 체인으로 여러 개의 인증서가 있으면 여러 개를 모두 같이 저장해놓으면 된다. pypi.org:443이 우리가 주로 원하는 사이트가 되지만, 아래는 정상 외부망에서도 시험해보기 위해 대표적인 시험사이트인 self-signed.badssl.com을 확인해보았다. https는 443 port를 통해 서비스되므로 포트번호는 443을 사용한다.
$ openssl s_client -showcerts -connect self-signed.badssl.com:443
CONNECTED(00000005)
depth=0 C = US, ST = California, L = San Francisco, O = BadSSL, CN = *.badssl.com
verify error:num=18:self signed certificate
verify return:1
depth=0 C = US, ST = California, L = San Francisco, O = BadSSL, CN = *.badssl.com
verify return:1
---
Certificate chain
0 s:C = US, ST = California, L = San Francisco, O = BadSSL, CN = *.badssl.com
i:C = US, ST = California, L = San Francisco, O = BadSSL, CN = *.badssl.com
-----BEGIN CERTIFICATE-----
MIIDeTCCAmGgAwIBAgIJAKvqfFfMqQaUMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y
MzA3MjEyMTU2MTJaFw0yNTA3MjAyMTU2MTJaMGIxCzAJBgNVBAYTAlVTMRMwEQYD
VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK
DAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2
PmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW
hyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A
xPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve
ww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY
QCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T
BAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI
hvcNAQELBQADggEBAKRIesYfOhb7rH1+Aw0B391ZHGkarzcSguAA3iKhhc8uzEf0
bOzByITqm2Fxdvrn8b1AJw4f3MnbbE3y4bWTbipdChEerou2qcjYPjJqOUH9lP+G
rn2OxtPzlznOrU5KlvHV6RMe5zvJMCXiTC4SuuKG7aBMz3jSfmP+Nf+n5q31g7xl
7tfnPfjnbYHyNcK/Y75uvl/IICYx6iaP6DJB8Ya4T/NlwKbpW1Av6zWQTi1GM5Cb
U4e00ZmGEr4Rtk+GIYmQ/hWY/IuerFXfnGOXdWPWAzZYwDtIc0bF5llfEABjfLjM
V5Yw9bcQWLPtjK/umfxzYB+jf7kjI9dLTCxJptE=
-----END CERTIFICATE-----
---
Server certificate
subject=C = US, ST = California, L = Sa...
이제 위에서 확보한 빨간색의 인증서 정보 라인들을 복사한 후 아래 아까 확인했던 cacert.pem 파일에 추가해보자.
(이 글의 맨 뒤에는 이보다 더 간단한 REQUESTS_CA_BUNDLE 환경변수 이용법도 추가되어있다. 먼저 시도해보자)
$ vi /Users/id/miniforge3/lib/python3.9/site-packages/certifi/cacert.pem
맨 마지막에 아래와 같은 방법을 통해 추가함으로써. 연관된 주석들(# Issuer 등)을 잘 살려서 추가해야 제대로 추가되었는지 확인이 가능하다. test.cer이라는 파일에 몇가지 주석 정보와 아까 복사해둔 인증서 정보를 추가해두자. 아래와 같은 명령과 복사 후 붙여넣기로 가능하다.
$ cat > test.cer
# Issuer: CN=badssl.com
# Subject: CN=badssl.com
# Label: "badssl.com selfsigned"
-----BEGIN CERTIFICATE-----
MIIC0DCCAbigAwIBAgIJAIwcJbJloMREMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
BAMTDWtiYW5rd2l0aC5jb20wHhcNMTYwOTEzMDI0OTQwWhcNMjYwOTExMDI0OTQw
WjAYMRYwFAYDVQQDEw1rYmFua3dpdGguY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEA0o889n3eHjdNCGvDj5a6Pv/2geZQ1jbEIL/o4w7h/v7USFpv
qefLgoVSmNuJnw9uq19T4gqbj26BMzDsjLeRczWnTouSzS5AGdag0LFu+B0E7L44
D4+8mAsaKOdyOa8cNYFzED1II5sudcE7WjFBd7KHygN+SItj0j86MV/dc9YJ/Gv4
Zppjzu/Mz/s1EgQ6TlldxZNHqCAuEAiVUlg3tdQ+YCC9V1XNGEm4BVD2Q4YdBN0e
0vDe+oS505TrfyuW2mbBr8OOon5wt+L76ZmrDG5TSwW+89mHmf/rY8CQb9AEb/4/
nCqHiN7QkkQhA2Tgo4nCipYZ3MxSLYzy0MCEtQIDAQABox0wGzAMBgNVHRMEBTAD
AQH/MAsGA1UdDwQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAzPm0NHfRhZaLlKKU
XA+6z0GENR7NYeHzGecLCD+4GgeL2Z4feSb7hflveW5m3mSSAibznOAg3wcwD6Am
ida86SHRxfEaZOuENM0tngPeWTobuxNPYMwWNNy//weuZVElr6LZcS+dLCe7qvhS
oIeaEJstT/MTTG6864iKLewtP/5fa8l21fMOQws4WfPDuNukoDJRlv40zYxVQxEJ
M1NturoG4Yw5rGnAnQLCahUR6HIRjq7ynVNZTCnikfiqscQzGhsPOyIjDfWpVR9e
oLR/NbTIfvAFXwd8bxPLtIgPxWhi4CE1hO6vAM6p8wGUCXqpEhqyGc0IbyBTr3cS
axNRaw==
-----END CERTIFICATE-----
[Control+D]
$ cat test.cer >> /Users/id/miniforge3/lib/python3.9/site-packages/certifi/cacert.pem
앞서 언급했듯이 CERTIFICATE가 몇개여도 이어서 추가하면 된다. 그러면 이제 정상적으로 이 인증서가 추가되었는지 확인해보자.
$ python check_certifi.py
...
<Name(CN=badssl.com)> 2026-09-11 02:49:40
상기처럼 # Issuer에 주석으로 넣었던 정보들이 출력됨을 위와 같이 확인했다. 인증서 추가를 확인하였으니 다시 한번 이 사이트 접속을 시험해보자.
$ python check_badssl.py
그러면 이전과 달리 아무런 오류가 나지 않음을 알 수 있다. 물론 pypi.org의 변조된 인증서도 위와 같이 openssl 명령으로 확인하여 복사&추가해두면 pip install도 문제없이 실행된다. 내부 정보보호를 위해 전체 사이트의 변조된 인증서를 가지는 금융권 등의 인터넷망에서 python을 잘 설치하는 방법이 되겠다.
추가)
또다른 간단한 방법으로는 아예 REQUESTS_CA_BUNDLE을 환경변수로 등록하는 방법이 있다. 이 경우는 매우 소수의 인증서만 인증되면 상관없을때 유용하게 사용된다. 앞서 cer을 곧바로 기본으로 사용하게 강제한다.
$ export REQUESTS_CA_BUNDLE=/User/xxx/test.cer
$ python check_badssl.py
하면 정상적으로 나오는 것을 확인할 수 있다.
'IT 실무 (환경구성)' 카테고리의 다른 글
Python package를 폐쇄망에 설치하기 (pip3) (4) | 2021.01.05 |
---|---|
폐쇄망에서의 redhat(RHEL) 혹은 CentOS rpm /yum 패키지 설치 방법 (5) | 2020.12.27 |
서버 인증서 설치의 내부 메카니즘/원리, 폐쇄망 엔지니어를 위한 글 (5) | 2020.12.27 |