The safest way to use Python SSL is to let ssl.create_default_context() build your context and never turn off its verification. The Python ssl module already ships with sane, secure defaults, so most TLS problems come from code that overrides those defaults rather than from the library itself. This guide walks through what the defaults do, how to load your own certificate authorities, and the anti-patterns that quietly disable protection.
What the ssl module gives you by default
ssl.create_default_context() returns an SSLContext configured for the common case: it disables SSLv2 and SSLv3, selects strong cipher suites, drops RC4 and unauthenticated ciphers, and loads the operating system's trusted CA store. When you create a client context with the default Purpose.SERVER_AUTH, it sets verify_mode to CERT_REQUIRED and turns on check_hostname. That combination is what actually protects you against a machine-in-the-middle: the server must present a certificate that chains to a trusted CA, and the hostname in that certificate must match the host you asked for.
A minimal, correct client looks like this:
import socket
import ssl
context = ssl.create_default_context()
with socket.create_connection(("example.com", 443)) as sock:
with context.wrap_socket(sock, server_hostname="example.com") as ssock:
print(ssock.version())
ssock.sendall(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
print(ssock.recv(1024))
Notice that server_hostname is passed to wrap_socket. Without it, check_hostname has nothing to compare against and the handshake fails, which is the correct, fail-closed behavior.
Where SSL Python code usually goes wrong
Almost every real-world TLS bug in Python traces back to someone silencing an error instead of fixing it. The two lines below are the ones to watch for in review:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
Together they turn a secure connection into an encrypted-but-unauthenticated one. An attacker who can intercept traffic can present any certificate, and your client will happily continue. This pattern usually appears because a developer hit an SSLCertVerificationError against a staging server with a self-signed cert and reached for the fastest silence. The same danger shows up in the requests library as verify=False and in urllib3 as disabled warnings.
If you see either of these in production code, treat it as a finding, not a style preference. An SCA tool such as Safeguard can flag insecure transport configuration transitively when it appears inside a dependency you did not write, but your own code is on you to review.
Trusting an internal or self-signed certificate
The legitimate reason people disable verification is that they need to talk to an internal service whose certificate is not signed by a public CA. The correct fix is to trust that specific CA, not to trust everything. Point the context at your CA file:
context = ssl.create_default_context(cafile="/etc/pki/internal-ca.pem")
Or load additional certificates onto an existing context:
context.load_verify_locations(cafile="internal-ca.pem")
This keeps check_hostname and CERT_REQUIRED in force. You are adding a trust anchor, not removing the check. If your internal certificate uses an IP address rather than a hostname, make sure the certificate carries the IP in its subjectAltName, because modern Python validates SANs and ignores the legacy common name.
Mutual TLS with a client certificate
When a server requires the client to authenticate as well, load your client cert and key into the context before wrapping the socket:
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
context.load_cert_chain(certfile="client.crt", keyfile="client.key")
On the server side you flip the purpose to Purpose.CLIENT_AUTH and set verify_mode = ssl.CERT_REQUIRED so the server rejects clients that do not present a trusted certificate. Keep the private key file readable only by the service account that needs it; a leaked client key is as good as a stolen password.
Pinning protocol versions and ciphers
The defaults already forbid the badly broken protocols, but you can raise the floor further. To require TLS 1.2 or newer:
context.minimum_version = ssl.TLSVersion.TLSv1_2
Avoid hand-rolling a cipher string with set_ciphers() unless you have a concrete compliance requirement. The Python ssl library and the underlying OpenSSL build already pick a modern list, and a custom string tends to rot as new attacks retire old ciphers. If you must pin, document why and set a reminder to revisit it.
Testing that verification actually works
A verification setting you never exercise is a setting you cannot trust. Two quick checks belong in your test suite. First, connect to a host with a known-good certificate and assert the handshake succeeds. Second, connect to a host with a deliberately bad certificate and assert that it raises ssl.SSLCertVerificationError. The badssl.com family of test endpoints (expired, self-signed, wrong-host) is built exactly for this. If your "secure" client connects to self-signed.badssl.com without raising, verification is off somewhere and you have just proven it.
For a broader view of how transport security fits into dependency and application scanning, our SCA product overview covers where these checks surface in a pipeline, and the Safeguard Academy has walkthroughs on secure defaults.
FAQ
Is the Python ssl module safe to use out of the box?
Yes, as long as you build your context with ssl.create_default_context() and do not override check_hostname or verify_mode. The defaults require a valid, hostname-matching certificate chained to a trusted CA, which is what you want in production.
Why do I get SSLCertVerificationError and how should I fix it?
It means the server's certificate could not be validated against your trust store, or the hostname did not match. Fix it by trusting the correct CA with cafile or load_verify_locations, not by disabling verification. If it is a public site, the real problem is usually an outdated CA bundle or a genuinely misconfigured server.
What is the difference between the ssl library and the requests library?
The ssl library is the low-level standard-library module that wraps sockets and manages TLS contexts. Requests is a higher-level HTTP client that uses ssl underneath. In requests you keep verification on by leaving verify=True (the default) or by pointing verify at a CA bundle path.
Should I ever set CERT_NONE?
Almost never in production. The only defensible use is a throwaway local experiment where no sensitive data crosses the wire. If you find CERT_NONE in a real service, treat it as a security bug and replace it with proper CA trust.