编程语言
UNSAFE_LEGACY_RENEGOTIATION_DISABLED:Python 报错怎么修
[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled 的意思是:服务器不支持 RFC 5746 规定的安全重协商,你的客户端用的 OpenSSL 3 拒绝和它建立连接。 证书本身没有问题。问题出在服务器,或者它前面的负载均衡、防火墙、HTTPS 解密设备,用的 TLS 实现早于 2010 年的那次修补,而 OpenSSL 3.0 默认不再迁就这种情况。
这条报错是什么意思
这是 OpenSSL 3 对未打 RFC 5746 补丁的服务器的默认拒绝。2009 年,TLS 重协商被发现存在漏洞(CVE-2009-3555),攻击者可以把自己的数据拼接到受害者会话的前面。2010 年 2 月发布的 RFC 5746 用 renegotiation_info 扩展修补了它:打过补丁的客户端和服务器会互相证明,重协商属于同一条连接。从来不发这个扩展的服务器就叫“未打补丁”。
OpenSSL 文档写明:如果 SSL_OP_LEGACY_SERVER_CONNECT 和 SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION 都没设置,和未打补丁服务器的首次连接就会失败。OpenSSL 1.1.1 默认设置了前者,OpenSSL 3.0 起不再默认设置。所以这个错误通常出现在换到 Ubuntu 22.04(自带 OpenSSL 3.0.2)、更新 Docker 基础镜像、或者换成链接 OpenSSL 3 的 Python 之后,而服务器可能多年没动过。
在 Python 里它经常被包在别的报错里,比如 requests 的 Max retries exceeded,AWS CLI 的输出里也会出现。其他基于 OpenSSL 3 的客户端会显示同一个原因:error:0A000152:SSL routines::unsafe legacy renegotiation disabled。
常见原因(按出现频率排序)
这个报错有 3 种情况:服务器或设备老旧(最常见)、链路上的 HTTPS 解密设备没打补丁、客户端刚换成 OpenSSL 3。
1. 老旧服务器或设备不支持 RFC 5746
多年未升级的应用服务器、嵌入式设备、老式硬件负载均衡、过时的 WAF 最常见,对接长期没人维护的老接口时容易碰到。
- 确认: 只在测试命令里临时打开旧式连接,看重协商那一行:
openssl version
openssl s_client -connect example.com:443 -servername example.com -legacy_server_connect </dev/null 2>/dev/null | grep -i renegotiation
# Secure Renegotiation IS NOT supported -> 服务器未打补丁
- 修复: 升级服务器的 TLS 库或设备固件。2010 年以后还在维护的 TLS 实现都支持 RFC 5746。启用 TLS 1.3 也能解决支持 TLS 1.3 的客户端的问题,因为 TLS 1.3 完全禁止重协商。
2. 链路上有 HTTPS 解密设备
如果 openssl s_client 显示的签发者是公司的上网行为管理、安全网关厂商,而不是公开 CA,那么“未打补丁”的是这台设备,而不是目标网站。见企业 SSL 解密导致的报错。
- 修复: 请网络管理员升级设备,或者把目标域名加入解密白名单。
3. 客户端换成了 OpenSSL 3
这是触发条件而不是根因,但它解释了“昨天还好好的”。查看 Python 用的 OpenSSL 版本:
python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
怎么和对方沟通
如果服务器不归你管,把下面几样东西发给对方的运维:报错原文、你这边的 OpenSSL 版本、上面那条带 -legacy_server_connect 的检测命令和它的输出。说明问题是“服务器不支持 RFC 5746 安全重协商”,而不是证书过期或域名不对,可以省去很多来回沟通。对方升级之前,你这边的临时方案要写清楚负责人和预计删除日期。
分平台修复命令
首选是修服务器。在等待对方升级期间,只对这一个目标放开旧式连接,并且保持证书校验开启。
Python 3.12 及以上,配合 requests,只作用于一台主机:
import ssl
import requests
from requests.adapters import HTTPAdapter
class LegacyServerConnectAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = ssl.create_default_context() # 证书校验仍然开启
ctx.options |= getattr(ssl, "OP_LEGACY_SERVER_CONNECT", 0x4)
kwargs["ssl_context"] = ctx
return super().init_poolmanager(*args, **kwargs)
session = requests.Session()
session.mount("https://legacy.example.com/", LegacyServerConnectAdapter())
print(session.get("https://legacy.example.com/", timeout=10).status_code)
Python 3.11 及更早版本没有这个常量,0x4 是 OpenSSL 头文件里 SSL_OP_LEGACY_SERVER_CONNECT 的值,所以代码里用 getattr 兜底。
任何链接 OpenSSL 3 的程序(Python,以及用 OpenSSL 编译的 curl、git),Linux 或 macOS 上只对一个进程生效:
# /etc/ssl/openssl-legacy-connect.cnf
openssl_conf = openssl_init
[openssl_init]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
Options = UnsafeLegacyServerConnect
OPENSSL_CONF=/etc/ssl/openssl-legacy-connect.cnf python3 job.py
OPENSSL_CONF 只替换这个进程的配置,不要用它覆盖 /etc/ssl/openssl.cnf。
Node.js 17 及以上(同样基于 OpenSSL 3):
const https = require('node:https');
const { constants } = require('node:crypto');
https.get('https://legacy.example.com/', { secureOptions: constants.SSL_OP_LEGACY_SERVER_CONNECT }, res => console.log(res.statusCode));
Windows: python.org 安装包自带 OpenSSL,上面的 Python 写法同样适用。
国内环境: 对接银行、政务或老 ERP 接口时,对方往往不能马上升级。可以先确认 TLS 终止点在哪里:如果对方前面有云厂商的负载均衡或 CDN,让对方把证书和 TLS 终止挪到负载均衡上,通常比升级老后端快得多。在等待期间,把上面的临时方案限定在一个独立的脚本或容器里运行,不要放进公共的基础镜像。
不推荐:关闭证书校验
verify=False、CERT_NONE、curl -k 对这个错误没有用:握手在检查证书之前就失败了。加上它们只会去掉防冒充的保护,链路上任何人都能截获 token 和密码,而程序照样报错。
同样不要用范围更大的 UnsafeLegacyRenegotiation / SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION,也不要修改全系统的 OpenSSL 配置,那会削弱这台机器上所有 TLS 客户端的安全性。更安全的做法是修服务器,或者只在一个上下文、一台主机上设置 OP_LEGACY_SERVER_CONNECT,证书校验保持开启,服务器升级后立刻删掉。把它当作有到期日的临时例外来管理。
怎么确认已经修好
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | grep -E "Secure Renegotiation|Protocol"
服务器修好后,不加 -legacy_server_connect 也能连上,并显示 Secure Renegotiation IS supported(TLS 1.2)或协商出 TLSv1.3。然后删掉客户端的临时代码,用默认设置重新跑一遍 Python 程序。也可以用 SSL 检测工具查看服务器支持的协议版本和加密套件,用 TLS 配置生成器生成一份现代的服务器配置。想了解重协商在握手中的位置,可以读 TLS 握手过程。
常见问题
为什么升级系统或 Python 之后才出现?
OpenSSL 1.1.1 默认允许和未打补丁的服务器建立首次连接,OpenSSL 3.0 不再允许。换成链接 OpenSSL 3 的发行版或 Python(例如 Ubuntu 22.04 自带 OpenSSL 3.0.2),服务器一点没变也会连不上。用 python3 -c "import ssl; print(ssl.OPENSSL_VERSION)" 查看当前版本。
OP_LEGACY_SERVER_CONNECT 有风险吗?
有。它重新打开了 RFC 5746 要堵住的那类攻击(CVE-2009-3555,通过重协商在会话前面注入数据),但只影响你设置了它的连接。只对那一台主机启用,证书校验保持开启,并把它当成临时方案,服务器修好就删掉。
UnsafeLegacyServerConnect 和 UnsafeLegacyRenegotiation 有什么区别?
UnsafeLegacyServerConnect(SSL_OP_LEGACY_SERVER_CONNECT)只允许 OpenSSL 客户端连接未打补丁的服务器;UnsafeLegacyRenegotiation(SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION)则允许和未打补丁的客户端、服务器双向做不安全的重协商,范围大得多。应该用范围更小的 UnsafeLegacyServerConnect。
服务器管理员怎么从外部自查?
运行 openssl s_client -connect example.com:443 -servername example.com -legacy_server_connect </dev/null | grep Renegotiation,受影响的服务器会显示 Secure Renegotiation IS NOT supported。能协商 TLS 1.3 的服务器不受影响,因为 TLS 1.3 禁止重协商。