본문 바로가기

IT 실무 (환경구성)

python 보안환경에서 구동하기, 인증서 문제(SSL_CERTIFICATE_VERIFY_FAILED)를 겪고 계신가요?

반응형

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

 

하면 정상적으로 나오는 것을 확인할 수 있다.

 

반응형