SSL Error: Unsafe Legacy Renegotiation Disabled
Problem Statement
When using Python to connect to certain HTTPS servers, you might encounter an error similar to:
requests.exceptions.SSLError: HTTPSConnectionPool(host='ssd.jpl.nasa.gov', port=443): Max retries exceeded with url: /api/horizons.api?format=text&EPHEM_TYPE=OBSERVER&QUANTITIES_[...]_ (Caused by SSLError(SSLError(1, '[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled (_ssl.c:997)')))
This error typically occurs when:
- Your client is using OpenSSL 3 or later
- The server doesn't support RFC 5746 secure renegotiation
- You're connecting to older infrastructure that uses legacy SSL/TLS renegotiation
SECURITY CONSIDERATIONS
Enabling legacy renegotiation makes your SSL connections vulnerable to the Man-in-the-Middle prefix attack described in CVE-2009-3555. Only use these solutions when connecting to trusted internal servers or when no alternative exists.
Recommended Solutions
Solution 1: Custom SSL Context with Legacy Support (Python)
This approach creates a custom SSL context that allows legacy renegotiation while maintaining other security features:
import ssl
import requests
import urllib3
class CustomHttpAdapter(requests.adapters.HTTPAdapter):
"""Transport adapter that allows custom SSL context"""
def __init__(self, ssl_context=None, **kwargs):
self.ssl_context = ssl_context
super().__init__(**kwargs)
def init_poolmanager(self, connections, maxsize, block=False):
self.poolmanager = urllib3.poolmanager.PoolManager(
num_pools=connections, maxsize=maxsize,
block=block, ssl_context=self.ssl_context)
def get_legacy_session():
"""Create a requests session that allows legacy SSL connections"""
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ctx.options |= 0x4 # OP_LEGACY_SERVER_CONNECT flag
session = requests.session()
session.mount('https://', CustomHttpAdapter(ctx))
return session
# Usage
response = get_legacy_session().get("https://your-target-url.com")
Solution 2: Using urllib with Custom Context
For applications using urllib directly:
import urllib.request
import ssl
# Create custom SSL context
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ctx.options |= 0x4 # Enable legacy renegotiation
# Make request
url = "https://your-target-url.com"
response = urllib.request.urlopen(url, context=ctx)
data = response.read().decode("utf-8")
Solution 3: For aiohttp Users
If you're using aiohttp for asynchronous requests:
import aiohttp
import ssl
custom_ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
custom_ssl_context.options |= 0x00040000 # SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
connector = aiohttp.TCPConnector(ssl=custom_ssl_context)
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get(url) as response:
return await response.text()
DEVELOPMENT ONLY
For testing purposes only, you might disable SSL verification (not recommended for production):
custom_ssl_context.check_hostname = False
custom_ssl_context.verify_mode = ssl.CERT_NONE
Alternative Approaches
OpenSSL Configuration Method
Create a custom OpenSSL configuration file (custom_openssl.cnf
):
openssl_conf = openssl_init
[openssl_init]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
[system_default_sect]
Options = UnsafeLegacyRenegotiation
Then set the environment variable before running your Python script:
export OPENSSL_CONF=/path/to/custom_openssl.cnf
python your_script.py
Package Management Solutions
NOT RECOMMENDED
These approaches downgrade security and should be temporary solutions only.
For pip environments:
pip install cryptography==36.0.2
For conda environments:
conda install openssl=1
Understanding the Technical Context
This error occurs because OpenSSL 3.0+ disabled support for insecure legacy renegotiation by default to protect against security vulnerabilities. Some older servers, particularly embedded systems or legacy infrastructure, haven't implemented the secure renegotiation extension (RFC 5746).
The hexadecimal value 0x4
corresponds to the OpenSSL option SSL_OP_LEGACY_SERVER_CONNECT
, which isn't directly exposed in Python's ssl module but can be set using the raw value.
Best Practices
- Prefer server updates: Whenever possible, update the server to support modern SSL/TLS standards
- Isolate legacy connections: Use the custom session approach only for specific problematic domains
- Monitor for updates: Regularly check if the server has been updated to remove the need for legacy support
- Document security implications: Keep track of where you've implemented these workarounds
Conclusion
While the "unsafe legacy renegotiation disabled" error can be frustrating, Python provides flexible SSL context configuration that allows you to work with legacy servers when necessary. The custom HTTP adapter approach is generally preferred as it minimizes the security impact by isolating the legacy behavior to specific connections rather than affecting your entire Python environment.
Remember that these solutions should be temporary measures while working toward more secure server configurations that comply with modern SSL/TLS standards.