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

企业内网场景下 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 文件会丢失所有公共根证书,必须先合并:

python 复制代码
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())
bash 复制代码
export REQUESTS_CA_BUNDLE=/path/to/combined-ca-bundle.pem

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

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

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

python 复制代码
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 上按域名匹配:

python 复制代码
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 为例:

python 复制代码
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。但若服务器端未发送完整证书链 (缺少中间证书),客户端需手动加载:

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

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

python 复制代码
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。两种注入方式:

构建时注入:

dockerfile 复制代码
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

运行时挂载:

bash 复制代码
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 管理系统,流水线启动时动态合并:

yaml 复制代码
# 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() 规范化

生产环境配置模板

python 复制代码
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 错误排查方向在客户端信任链或服务器证书链,不在代理参数。

相关推荐
budaoweng06091 小时前
charles报错HTTP/1.1 200 Connection established
网络·网络协议·http
Java面试题总结1 小时前
使用 Python 在 Excel 中添加和自定义文本框
开发语言·python·excel
geovindu2 小时前
CSharp: Recursion Algorithm
开发语言·后端·算法·c#·递归算法
dsyyyyy11012 小时前
用JavaScript实现排序算法
开发语言·javascript·排序算法
万联WANFLOW2 小时前
月之暗面发布 Kimi K3:全球首个开源 3T 级大模型,前端编程竞技场登顶第一
网络·人工智能·架构·业界资讯
IPDEEP全球代理2 小时前
住宅IP、移动IP、机房IP有什么区别?
网络·网络协议·tcp/ip
m沐沐2 小时前
【机器学习】基于 dlib 面部关键点的多表情分类
人工智能·python·深度学习·机器学习·计算机视觉·人脸识别·表情识别
CHANG_THE_WORLD2 小时前
逐层拆解:C++ 虚函数从对象内存到手工调用的完整过程
java·开发语言·c++
Scott9999HH2 小时前
2026 避坑实录:国产品牌压力变送器什么牌子好?从硬件抗扰到 C++ Qt 实时曲线绘制源码剖析
开发语言·c++·qt