PAYATHON 2026

mod_wsgi 访问 HTTPS 9090 端口时证书校验失败,如何排查?

数据小陈

结论

:9090 本身不会导致证书校验失败。HTTPS 证书的主机名校验针对 myexample.com,不包含端口号。不过,同一域名的 443 和 9090 端口可能由不同服务处理,提供的证书和证书链也可能不同。

unable to get local issuer certificate 表示客户端无法使用服务端提供的证书链和本地信任证书,建立到受信任 CA 的验证路径。应先检查 9090 端口实际提供的证书链,再确认签发它的 CA 是否受到当前 Python 进程信任。现有信息还不足以确定具体缺少哪张证书。

为什么 443 成功,9090 却失败?

问题中的代码使用 certifi 提供的 CA 信任库:

context = ssl.create_default_context(cafile=certifi.where())
req = urlopen(url, context=context)

ssl.create_default_context(...) 创建 TLS 配置并读取 CA 文件,此时还没有连接目标地址。如果错误随目标端口变化,通常是在 urlopen(...) 发起连接、执行 TLS 握手时触发的。

常见原因包括:

  • 443 由 Apache2 处理,配置了完整证书链;9090 由另一个服务处理,遗漏了中间证书。
  • 9090 使用自签名证书,或使用内部 CA 签发的证书,而该 CA 不在 certifi 信任库中。
  • 9090 的 TLS 配置或 SNI 配置有误,返回了其他证书或不完整的证书链。

Apache2 上的网站证书配置正确,不能证明另一个端口的 TLS 配置也正确。即使两个服务使用相同的服务器证书,各自发送的中间证书也可能不同。

这里的校验是客户端验证服务端身份。mod_wsgi 本身不会让所有 HTTPS 请求都必须提供客户端证书;只有目标服务明确配置了双向 TLS,才涉及客户端证书认证。

1. 比较两个端口提供的证书链

在运行 Python 请求的机器上分别执行:

openssl s_client -connect myexample.com:443 -servername myexample.com -showcerts </dev/null
openssl s_client -connect myexample.com:9090 -servername myexample.com -showcerts </dev/null

比较服务器证书的签发者、有效期,以及服务端发送的中间证书。还应检查证书的 Subject Alternative Name 是否包含 myexample.com。

-servername myexample.com 用于发送 SNI,帮助服务器选择对应域名的证书。比较时应保留该参数。

命令行 OpenSSL 默认使用的信任库可能与 Python 的 certifi 不同,因此不能只凭它的默认验证结果判断 Python 一定能够通过。需要对照时,可以指定同一份 CA 文件:

openssl s_client \
  -connect myexample.com:9090 \
  -servername myexample.com \
  -CAfile /path/to/ca-bundle.pem \
  -verify_return_error \
  -verify_hostname myexample.com </dev/null

将 /path/to/ca-bundle.pem 替换为应用中 certifi.where() 返回的路径。若命令行与 mod_wsgi 的结果不同,再检查两者是否使用相同的 Python 环境、代理配置和目标地址。

2. 根据证书来源修复

公共 CA 证书:补全服务端证书链

如果 9090 使用公共 CA 签发的证书,应在负责该端口 TLS 的服务中配置服务器证书及必要的中间证书。通常服务器证书在前,中间证书随后;根证书通常无需发送。

具体配置项取决于监听 9090 的软件。先确认 TLS 由 Apache2、反向代理还是独立应用终止,再修改对应服务的配置。不能只修改 443 端口的 Apache2 配置。

修复后,Python 可以继续使用证书校验:

import ssl
from urllib.request import urlopen

import certifi

url = "https://myexample.com:9090"
context = ssl.create_default_context(cafile=certifi.where())

with urlopen(url, context=context, timeout=30) as response:
    data = response.read()

如果怀疑 certifi 的 CA 数据过旧,可以检查部署环境中的版本并按项目流程更新。更新信任库不能替代服务端缺失的中间证书。

内部 CA 证书:显式加载可信 CA

如果该服务使用内部 CA,certifi 不包含它可能是正常情况。确认 CA 证书来源可信后,将其加载到应用的信任配置中:

import ssl
from urllib.request import urlopen

import certifi

context = ssl.create_default_context(cafile=certifi.where())
context.load_verify_locations(cafile="/path/to/internal-root-ca.pem")

with urlopen(
    "https://myexample.com:9090",
    context=context,
    timeout=30,
) as response:
    data = response.read()

这段代码保留 certifi 的信任证书,并额外加载内部 CA。服务端仍应发送必要的中间证书,证书中的主机名也必须匹配请求地址。不要直接把连接过程中收到的未知证书加入信任库。

3. 不要把关闭校验当作修复

ssl._create_unverified_context() 会关闭证书验证,包括证书链和主机名检查。它能让部分请求通过,可以帮助判断失败是否与证书校验有关,但不能证明目标服务器的身份可信。

如果关闭校验后仍然失败,需要查看那次请求的具体异常。连接超时、代理错误、TLS 协议问题和 HTTP 错误都可能独立发生,不能仅凭这一现象认定是“因为不安全而被拒绝”。

最初出现的 HTTP 503 也需要单独核实。它表示相关服务暂时无法完成请求,可能由代理、网关或后端产生。应结合 Apache2、代理和后端服务在同一时间的日志定位问题。现有信息不足以证明把 HTTP 改成 HTTPS 就能解决。

备注:内容仅供参考。