Enforce TLS 1.3 with Python ssl.SSLContext

This guide outlines the precise settings required to strictly enforce TLS 1.3 encryption using Python's standard ssl.SSLContext. Enforcing modern transport security protects applications from legacy cryptographic vulnerabilities by restricting negotiations solely to the TLS 1.3 protocol. Below, you will find the prerequisite environment requirements, context initialization methods, protocol constraints, and a complete code example.

Prerequisites

To use TLS 1.3 in Python, ensure your environment meets the following baseline requirements:

You can verify your OpenSSL version in Python with:

import ssl
print(ssl.OPENSSL_VERSION)

Essential ssl.SSLContext Configurations

To mandate TLS 1.3 and reject all previous protocols (such as TLS 1.2, TLS 1.0, and SSLv3), apply the following properties to your context:

1. Initialize Context with Modern Protocol Constants

Avoid legacy protocol constants such as ssl.PROTOCOL_TLSv1_2 or ssl.PROTOCOL_SSLv23. Instead, initialize using standard purpose-based constructors:

2. Restrict Protocol Versions

The most reliable way to enforce TLS 1.3 is by defining explicit version bounds using ssl.TLSVersion:

context.minimum_version = ssl.TLSVersion.TLSv1_3
context.maximum_version = ssl.TLSVersion.TLSv1_3

Setting both minimum_version and maximum_version ensures that the handshake will immediately fail if the remote peer cannot negotiate TLS 1.3.

3. Handling Cipher Suites

Unlike earlier protocols, TLS 1.3 uses a completely distinct set of cipher suites (e.g., TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_GCM_SHA256).

The standard context.set_ciphers() method only affects TLS 1.2 and earlier ciphers in OpenSSL. By locking the protocol version strictly to ssl.TLSVersion.TLSv1_3, the context automatically uses OpenSSL's secure, built-in TLS 1.3 cipher suites.

Complete Client Implementation

Below is a complete implementation showing how to enforce TLS 1.3 when establishing a client socket:

import socket
import ssl

hostname = "example.com"
port = 443

# 1. Initialize the client context
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

# 2. Load standard system CA certificates
context.load_default_certs()

# 3. Restrict negotiation strictly to TLS 1.3
context.minimum_version = ssl.TLSVersion.TLSv1_3
context.maximum_version = ssl.TLSVersion.TLSv1_3

# 4. Enforce certificate validation and hostname checking
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True

# 5. Connect and verify
with socket.create_connection((hostname, port)) as sock:
    with context.wrap_socket(sock, server_hostname=hostname) as ssock:
        print(f"Negotiated Protocol: {ssock.version()}")
        print(f"Negotiated Cipher: {ssock.cipher()}")

Complete Server Implementation

For a server, supply your certificate and private key, while locking down the same version boundaries:

import socket
import ssl

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)

# Load server certificate and private key
context.load_cert_chain(certfile="cert.pem", keyfile="key.pem")

# Enforce TLS 1.3 only
context.minimum_version = ssl.TLSVersion.TLSv1_3
context.maximum_version = ssl.TLSVersion.TLSv1_3

# Bind and listen
bind_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
bind_socket.bind(("0.0.0.0", 8443))
bind_socket.listen(5)

with context.wrap_socket(bind_socket, server_side=True) as server_sock:
    conn, addr = server_sock.accept()
    with conn:
        print(f"Connection accepted from {addr} using {conn.version()}")