企业内网场景下 Python 自定义 CA 证书信任链的正确配置方法

Python 在内网访问 HTTPS 接口时,常遇到 SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate。多数人用 verify=False 绕过验证,但这等于关闭了 TLS 的身份校验。正确做法是将企业 CA 根证书加入 Python 的信任链,同时保留对公网证书的正常校验。

内网自定义 CA 的来源

企业内网的自定义 CA 证书来自两类场景。

内部服务 (API 网关、GitLab、Harbor、监控平台等) 使用内部域名或 IP 地址,公共 CA 无法签发此类证书,企业自建 CA 签发后要求客户端信任其根证书。

网络安全设备 (防火墙、IDS、DLP) 对 HTTPS 流量做 MITM 检查,设备持有自签根证书,经其转发的连接会被重新签名。浏览器通过组策略信任该根证书,Python 不会。

两类场景的本质相同:程序需要信任一个不在公共 CA 信任链里的根证书。

Python 的证书验证机制

Python 的 ssl 模块加载 CA bundle (PEM 格式文件) 作为信任锚。默认路径可通过 ssl.get_default_verify_paths() 查看,各操作系统不同。

requests 库在此基础上使用 certifi 包提供的证书包 (certifi.where()),其中只包含 Mozilla 维护的公共根证书。遇到企业 CA 签名的证书时,在 certifi 证书包中找不到对应的根证书,验证失败。

配置方法

方法一: 环境变量指定合并证书包

requests 读取 REQUESTS_CA_BUNDLE 环境变量, ssl 模块读取 SSL_CERT_FILE。直接指定企业 CA 文件会丢失所有公共根证书,必须先合并:

import certifi, shutil

combined_path = "/path/to/combined-ca-bundle.pem"
shutil.copy(certifi.where(), combined_path)

with open(combined_path, "a") as f, \
     open("/path/to/company-root-ca.pem") as ca:
    f.write("\n")
    f.write(ca.read())
export REQUESTS_CA_BUNDLE=/path/to/combined-ca-bundle.pem

优点:不改现有代码。缺点:影响进程内所有 requests 调用;合并文件需随 certifi 更新手动重新生成。

方法二: verify 参数按请求指定

verify 参数只影响当前请求,不影响全局:

import requests

r = requests.get(
    "https://internal-api.company.com/v1/users",
    verify="/path/to/combined-ca-bundle.pem"
)

缺点:每个内网请求都要传参;合并文件维护问题不变。

方法三: 自定义 SSLContext + HTTPAdapter

通过 ssl.SSLContext 精细控制 TLS 配置,挂载到 HTTPAdapter 上按域名匹配:

import ssl, certifi, requests
from requests.adapters import HTTPAdapter

ctx = ssl.create_default_context()
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.set_ciphers("ECDHE+AESGCM:DHE+AESGCM:!aNULL:!MD5:!DSS")
ctx.load_verify_locations(certifi.where())
ctx.load_verify_locations("/path/to/company-root-ca.pem")

adapter = HTTPAdapter()
adapter.poolmanager.connection_pool_kw["ssl_context"] = ctx

session = requests.Session()
session.mount("https://internal-api.company.com", adapter)

r = session.get("https://internal-api.company.com/v1/users")

session.mount 按 URL 前缀匹配,内网请求走自定义 Adapter,公网请求走默认 SSLContext。此方法支持 TLS 版本和密码套件控制,适合等保合规场景。

亿牛云代理场景下的证书处理

亿牛云 (16yun.cn) 是企业级代理 IP 服务商,代理地址如 t.16yun.cn:31111,需用户名密码认证。HTTPS 请求经过正向代理时使用 CONNECT 方法建立加密隧道,代理仅转发流量不做 MITM。目标服务器使用公共 CA 证书时,走代理与直连行为一致,校验正常通过。

若目标在内网且使用企业 CA 证书,证书校验仍会失败,需将企业 CA 加入信任链。三种方法均可适用,以 Session + SSLContext 为例:

import ssl, certifi, requests
from requests.adapters import HTTPAdapter

ctx = ssl.create_default_context()
ctx.load_verify_locations(certifi.where())
ctx.load_verify_locations("/path/to/company-root-ca.pem")

adapter = HTTPAdapter()
adapter.poolmanager.connection_pool_kw["ssl_context"] = ctx

session = requests.Session()
session.mount("https://", adapter)
session.proxies = {
    "http": "http://username:password@t.16yun.cn:31111",
    "https": "http://username:password@t.16yun.cn:31111",
}

r = session.get("https://internal-api.company.com/v1/data")

若企业网络设备对代理流量做 MITM 审查,客户端收到的证书由防火墙签发而非目标服务器,需将防火墙 CA 加入信任链。

排查原则:亿牛云正向代理不做 MITM,SSL 错误的根源在客户端信任链配置或服务器端证书链配置,不在代理参数。

多级 CA 证书链

企业 CA 的典型结构为:根 CA 签发中间 CA,中间 CA 签发服务器证书。客户端加载根 CA 即可完成整条链的验证,无需单独加载中间 CA。但若服务器端未发送完整证书链 (缺少中间证书),客户端需手动加载:

# 检查服务器证书链完整性
openssl s_client -connect internal-api.company.com:443 -showcerts

若输出仅含服务器证书而无中间证书,说明服务器配置不完整。客户端可临时补丁:

ctx.load_verify_locations("/path/to/company-root-ca.pem")
ctx.load_verify_locations("/path/to/company-intermediate-ca.pem")

根本解决方案是让运维修复服务器端配置,使其发送完整证书链。

容器与 CI 环境

Python 官方 Docker 镜像基于 Debian,不含企业 CA。两种注入方式:

构建时注入:

FROM python:3.11
COPY company-root-ca.pem /usr/local/share/ca-certificates/company-root-ca.crt
RUN update-ca-certificates
RUN python -c "\
import certifi, shutil;\
shutil.copy(certifi.where(), '/tmp/combined.pem');\
with open('/tmp/combined.pem', 'a') as f:\
    with open('/usr/local/share/ca-certificates/company-root-ca.crt') as ca:\
        f.write(ca.read());\
shutil.copy('/tmp/combined.pem', certifi.where())"
ENV REQUESTS_CA_BUNDLE=/tmp/combined.pem

运行时挂载:

docker run \
  -v /path/to/combined-ca-bundle.pem:/etc/ssl/custom-ca.pem \
  -e REQUESTS_CA_BUNDLE=/etc/ssl/custom-ca.pem \
  my-python-app

CI/CD 环境中,将企业 CA 证书存入 secrets 管理系统,流水线启动时动态合并:

# GitLab CI 示例
variables:
  COMPANY_CA: $CI_COMPANY_CA_PEM
script:
  - python merge_ca.py
  - export REQUESTS_CA_BUNDLE=/tmp/combined.pem
  - python run_tests.py

常见错误

错误 原因 解决
DER 文件静默加载失败 load_verify_locations 只接受 PEM 格式,DER 文件不报错但不生效 openssl x509 -inform DER -in ca.cer -out ca.pem
证书链断裂 中间 CA 过期,根 CA 有效时整条链仍断 openssl x509 -in ca.pem -noout -dates 检查各层有效期
合合文件解析失败 两个 PEM 证书之间缺少换行分隔 合并时 f.write("\n") 确保分隔
Windows 环境变量路径异常 路径含空格或反斜杠 用正斜杠或 os.path.abspath() 规范化

生产环境配置模板

import ssl, certifi, requests
from requests.adapters import HTTPAdapter
from pathlib import Path

def create_ssl_context(ca_paths, min_tls=ssl.TLSVersion.TLSv1_2):
    ctx = ssl.create_default_context()
    ctx.minimum_version = min_tls
    ctx.set_ciphers("ECDHE+AESGCM:DHE+AESGCM:!aNULL:!MD5:!DSS")
    ctx.load_verify_locations(certifi.where())
    for ca_path in ca_paths:
        if not Path(ca_path).exists():
            raise FileNotFoundError(f"CA certificate not found: {ca_path}")
        ctx.load_verify_locations(ca_path)
    return ctx

def create_internal_session(ca_paths, domain_prefix, proxy_config=None):
    ctx = create_ssl_context(ca_paths)
    adapter = HTTPAdapter()
    adapter.poolmanager.connection_pool_kw["ssl_context"] = ctx
    session = requests.Session()
    session.mount(domain_prefix, adapter)
    if proxy_config:
        session.proxies = proxy_config
    return session

session = create_internal_session(
    ca_paths=["/etc/ssl/company-root-ca.pem"],
    domain_prefix="https://internal-api.company.com",
    proxy_config={
        "http": "http://user:pass@t.16yun.cn:31111",
        "https": "http://user:pass@t.16yun.cn:31111",
    },
)
r = session.get("https://internal-api.company.com/v1/users")

模板包含:TLS 1.2 强制最低版本、弱密码套件禁用、公共证书与企业 CA 合并加载、域名级 Adapter 匹配、可选代理配置、CA 文件路径校验。

总结

核心原则:将企业 CA 加入信任链,而非绕过验证。环境变量方式适合快速验证;verify 参数适合少量内网请求;自定义 SSLContext + HTTPAdapter 适合生产环境精细控制。无论哪种方法,合并证书包而非替换、确认 PEM 格式编码、定期检查 CA 有效期。代理场景下,正向代理不做 MITM,SSL 错误排查方向在客户端信任链或服务器证书链,不在代理参数。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐