Programming languages
Python UNSAFE_LEGACY_RENEGOTIATION_DISABLED: causes and fix
[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled means the server does not support RFC 5746 secure renegotiation, and your client's OpenSSL 3 refuses to connect to it. The certificate is fine. The server, or a device in front of it, runs a TLS stack older than the 2010 fix, which OpenSSL 3.0 no longer tolerates by default.
What the error means
OpenSSL 3 requires the RFC 5746 renegotiation extension by default, and this server does not send it. In 2009 a flaw in TLS renegotiation (CVE-2009-3555) let an attacker splice their own data in front of a victim's session. RFC 5746, published in February 2010, fixed it with the renegotiation_info extension: patched clients and servers prove to each other that a renegotiation belongs to the same connection. A server that never sends this extension is "unpatched".
OpenSSL's documentation is explicit: if neither SSL_OP_LEGACY_SERVER_CONNECT nor SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION is set, initial connections to unpatched servers fail. OpenSSL 1.1.1 set SSL_OP_LEGACY_SERVER_CONNECT by default; OpenSSL 3.0 no longer does. That is why the error typically shows up right after moving to Ubuntu 22.04 (which ships OpenSSL 3.0.2), a newer Docker base image, or a Python build linked against OpenSSL 3, while the server itself has not changed for years.
In Python you will usually see it wrapped, for example inside requests' Max retries exceeded or in AWS CLI output. Other OpenSSL 3 clients print the same reason: error:0A000152:SSL routines::unsafe legacy renegotiation disabled.
Causes, most common first
1. An old server or appliance without RFC 5746
Legacy application servers, embedded devices, old hardware load balancers and outdated web application firewalls are the usual suspects.
- Confirm: run OpenSSL 3 with the legacy option enabled only for the test, and read the renegotiation line:
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 -> server is unpatched
- Fix: update the server's TLS library or the appliance firmware. Any TLS stack maintained after 2010 supports RFC 5746. Enabling TLS 1.3 also removes the problem for clients that support it, because TLS 1.3 forbids renegotiation entirely.
2. A TLS-inspecting proxy in the path
If the issuer shown by openssl s_client is your company's proxy or security product rather than a public CA, the unpatched party is the proxy, not the website. See corporate SSL inspection.
- Fix: ask the network team to update the proxy, or to exempt the destination from inspection.
3. Your client moved to OpenSSL 3
This is the trigger rather than the cause, but it explains "it worked yesterday". Check which OpenSSL your Python uses:
python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
Fix commands by platform
The server-side fix is always preferred. While you wait for it, allow the legacy connection for that one destination only, and keep certificate verification on.
Python 3.12+ with requests, scoped to one host:
import ssl
import requests
from requests.adapters import HTTPAdapter
class LegacyServerConnectAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = ssl.create_default_context() # verification stays on
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)
On Python 3.11 and earlier the constant does not exist; 0x4 is the value of SSL_OP_LEGACY_SERVER_CONNECT in OpenSSL's headers, which is why the getattr fallback is there.
Any program linked against OpenSSL 3 (Python, and curl or git when they are built with OpenSSL), scoped to one process with a dedicated config file:
# /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 replaces the system configuration for that process only, so do not put this file in place of /etc/ssl/openssl.cnf.
Node.js 17+ (also built on 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: the python.org installer ships its own OpenSSL, so the Python fix above applies unchanged.
Don't disable certificate verification
verify=False, CERT_NONE or curl -k do not fix this error at all: the handshake fails before any certificate is checked. Adding them only removes protection against impersonation, so tokens and passwords can be intercepted by anyone on the path, and the code stays broken.
Also avoid the broader UnsafeLegacyRenegotiation / SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION and avoid editing the system-wide OpenSSL configuration, which would weaken every TLS client on the machine. The safer path is the one above: fix the server, or set OP_LEGACY_SERVER_CONNECT on one context, for one host, with verification on, and remove it once the server is updated. Track it like any other temporary exception.
Verify the fix
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | grep -E "Secure Renegotiation|Protocol"
After the server is fixed, this works without -legacy_server_connect and prints Secure Renegotiation IS supported (TLS 1.2) or shows TLSv1.3. Then remove the client workaround and rerun your Python code with default settings. You can also check the protocol versions and cipher suites the server offers in the SSL checker, and generate a modern server configuration with the TLS config generator. For background on how renegotiation fits into the handshake, see the TLS handshake.
FAQ
Why did this start after an operating system or Python upgrade?
Because OpenSSL 1.1.1 allowed initial connections to unpatched servers by default and OpenSSL 3.0 does not. Upgrading to a distribution or Python build that links OpenSSL 3, for example Ubuntu 22.04 with OpenSSL 3.0.2, is enough to make an old server fail even though nothing changed on the server. Check with python3 -c "import ssl; print(ssl.OPENSSL_VERSION)".
Is OP_LEGACY_SERVER_CONNECT dangerous?
It reopens the attack class that RFC 5746 was written to close (CVE-2009-3555, a prefix injection through renegotiation), but only for connections where you set it. Scope it to the single host that needs it, keep certificate verification on, and treat it as temporary until the server is fixed.
What is the difference between UnsafeLegacyServerConnect and UnsafeLegacyRenegotiation?
UnsafeLegacyServerConnect (SSL_OP_LEGACY_SERVER_CONNECT) only lets an OpenSSL client connect to unpatched servers. UnsafeLegacyRenegotiation (SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION) allows insecure renegotiation with unpatched clients and servers in both directions. Use the narrower UnsafeLegacyServerConnect.
Can the server owner check for this from outside?
Yes. openssl s_client -connect example.com:443 -servername example.com -legacy_server_connect </dev/null | grep Renegotiation prints Secure Renegotiation IS NOT supported for an affected server. A server that negotiates TLS 1.3 is not affected because TLS 1.3 forbids renegotiation.