TL;DR
Python không thể xác minh chứng chỉ SSL của server so với bundle CA tin cậy của nó. Hãy chọn cách sửa phù hợp với tình huống của bạn:
- macOS: chạy script
Install Certificates.commandđi kèm với Python. - Bất kỳ OS nào có
requests: nâng cấpcertifilà xong. - Chứng chỉ tự ký/doanh nghiệp: thêm chứng chỉ CA vào trust store của bạn.
Đừng dùng lối tắt verify=False. Nó âm thầm tắt toàn bộ bảo vệ SSL — và khiến bạn dễ bị tấn công man-in-the-middle trên môi trường production.
Nguyên nhân gây ra lỗi này
Mọi request HTTPS mà Python thực hiện đều phải qua bước kiểm tra chứng chỉ. Python so sánh chứng chỉ của server với một bundle các Certificate Authority (CA) tin cậy. Quá trình kiểm tra đó thất bại vì một số lý do phổ biến:
- Bundle CA của Python bị thiếu hoặc lỗi thời — rất hay gặp trên macOS cài mới.
- Server sử dụng chứng chỉ tự ký hoặc CA nội bộ của doanh nghiệp.
- Proxy doanh nghiệp (Zscaler, Charles, Fiddler) đang thực hiện SSL inspection và trình bày chứng chỉ của riêng nó.
- Module
sslcủa Python không thể truy cập system CA store.
Traceback đầy đủ trông như sau:
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url: /
Caused by: ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)
Cách sửa 1 — macOS: chạy trình cài đặt chứng chỉ
Các bản cài Python mới trên macOS bỏ qua system keychain hoàn toàn. Trình cài đặt đi kèm một bản sửa lỗi dùng một lần cho vấn đề này.
Mở Terminal và chạy:
/Applications/Python\ 3.x/Install\ Certificates.command
Thay 3.x bằng phiên bản của bạn — ví dụ, 3.12. Đang dùng pyenv hoặc Homebrew? Chạy lệnh này thay thế:
pip install --upgrade certifi
Sau đó xác minh Python đã tìm thấy bundle hợp lệ:
python -c "import ssl; print(ssl.get_default_verify_paths())"
Bạn cần thấy cafile hoặc capath không rỗng, trỏ đến một file thực sự tồn tại trên ổ đĩa.
Cách sửa 2 — cài đặt hoặc nâng cấp certifi
certifi đi kèm một bundle CA được tuyển chọn mà requests phụ thuộc vào. Nếu nó đã lỗi thời — hoặc bị thiếu — quá trình xác minh sẽ thất bại.
pip install --upgrade certifi
Đang làm việc trong virtualenv? Kích hoạt nó trước, nếu không bạn sẽ nâng cấp nhầm môi trường:
source venv/bin/activate
pip install --upgrade certifi
Kiểm tra bundle nào Python sẽ thực sự sử dụng:
python -c "import certifi; print(certifi.where())"
Sau đó chạy một bài kiểm tra nhanh:
import requests
r = requests.get('https://example.com')
print(r.status_code) # 200 nghĩa là thành công
Cách sửa 3 — thêm chứng chỉ CA tùy chỉnh hoặc của doanh nghiệp
Đang ở sau tường lửa doanh nghiệp? Các công cụ như Zscaler, Charles Proxy, hoặc PKI nội bộ của công ty bạn chèn CA riêng của họ vào mọi kết nối HTTPS. Python chưa từng biết đến CA đó — đó là nguyên nhân gây ra lỗi.
Lấy chứng chỉ CA từ bộ phận IT của bạn (thường là file .crt hoặc .pem), sau đó chọn một trong các cách sau:
Cách A: truyền đường dẫn chứng chỉ trực tiếp trong requests
import requests
r = requests.get('https://internal.company.com', verify='/path/to/company-ca.crt')
print(r.status_code)
Cách B: đặt biến môi trường
Hoạt động với bất kỳ thư viện nào tôn trọng REQUESTS_CA_BUNDLE hoặc SSL_CERT_FILE. Thêm vào shell profile của bạn để giữ lại sau khi khởi động lại.
# Linux / macOS — thêm vào ~/.bashrc hoặc ~/.zshrc
export REQUESTS_CA_BUNDLE=/path/to/company-ca.crt
export SSL_CERT_FILE=/path/to/company-ca.crt
# Windows (PowerShell)
$env:REQUESTS_CA_BUNDLE = "C:\certs\company-ca.crt"
Cách C: nối thêm chứng chỉ vào bundle của certifi
Hữu ích khi bạn cần sửa lỗi toàn cục cho tất cả script trong một môi trường:
import certifi
# Chạy một lần trong quá trình thiết lập môi trường
with open('/path/to/company-ca.crt', 'r') as f:
custom_cert = f.read()
certifi_bundle = certifi.where()
with open(certifi_bundle, 'a') as bundle:
bundle.write('\n' + custom_cert)
Lưu ý: nâng cấp certifi sau này sẽ xóa thay đổi này. Hãy chạy lại script sau khi nâng cấp.
Cách sửa 4 — urllib hoặc http.client (không dùng requests)
Không dùng requests? Thư viện chuẩn cần một SSL context thủ công:
import urllib.request
import ssl
import certifi
# Tạo context sử dụng bundle của certifi
ctx = ssl.create_default_context(cafile=certifi.where())
with urllib.request.urlopen('https://example.com', context=ctx) as response:
print(response.read())
Những điều KHÔNG nên làm
Stack Overflow đầy những câu trả lời gợi ý tắt xác minh:
# KHÔNG DÙNG TRÊN PRODUCTION
import requests
requests.get('https://example.com', verify=False) # không an toàn
# Cũng nguy hiểm
import ssl
ssl._create_default_https_context = ssl._create_unverified_context # không an toàn
Cả hai cách đều làm im lặng lỗi — nhưng chúng làm vậy bằng cách tắt hoàn toàn bước kiểm tra. Bất kỳ kẻ tấn công nào chặn kết nối đều có thể phục vụ chứng chỉ giả. Python sẽ không phàn nàn. Dữ liệu của bạn bị rò rỉ.
Trường hợp duy nhất có thể chấp nhận là script test tạm thời trỏ đến localhost và không có dữ liệu thật. Không bao giờ dùng với server bên ngoài.
Xác minh cách sửa đã có tác dụng
Chạy đoạn code này để xác nhận mọi thứ đã được kết nối đúng:
python - <<'EOF'
import requests
import ssl
import certifi
print('certifi bundle:', certifi.where())
print('ssl default paths:', ssl.get_default_verify_paths())
r = requests.get('https://httpbin.org/get')
print('HTTP status:', r.status_code) # kỳ vọng 200
EOF
Vẫn thất bại sau khi nâng cấp certifi? Virtualenv có thể vẫn đang trỏ đến phiên bản cũ. Kiểm tra bằng:
pip show certifi
Trường Location phải nằm trong virtualenv đang hoạt động của bạn — không phải đường dẫn Python hệ thống như /usr/lib/python3.
Hướng dẫn quyết định nhanh
- macOS, Python cài mới → Cách sửa 1 (chạy Install Certificates.command)
- Bất kỳ OS, chứng chỉ lỗi thời → Cách sửa 2 (nâng cấp certifi)
- Mạng doanh nghiệp / SSL inspection → Cách sửa 3 (thêm CA của công ty)
- Không dùng requests → Cách sửa 4 (urllib với certifi context)

