skip to content

How do you set a minimum TLS version on a Python ssl.SSLContext, and what does that not guarantee?

level: middleimportance: should knowfreq 32%

answer

  1. A bound on your side, not a promise
  2. An enum, not an option bit
  3. The library below may be stricter
  4. The default already refuses 1.0 and 1.1
  5. Ask the socket what it negotiated

basics

~20 s

Assign an ssl.TLSVersion member to SSLContext.minimum_version, for example ssl.TLSVersion.TLSv1_3. It only bounds what your side will negotiate; the peer, the linked OpenSSL build and the platform policy can each refuse more than you asked for.

solid answer

~40 s

Set `ssl.SSLContext.minimum_version` to a member of the `ssl.TLSVersion` enum - `TLSv1_2`, `TLSv1_3`, or the relative `MINIMUM_SUPPORTED`/`MAXIMUM_SUPPORTED` bounds. It is settable only on a general-purpose context (`ssl.PROTOCOL_TLS_CLIENT` or `ssl.PROTOCOL_TLS_SERVER`, which is what `ssl.create_default_context()` returns), and it superseded the old per-version option flags such as `ssl.OP_NO_TLSv1_1`. Since 3.10 the default context already floors at TLS 1.2, so the assignment matters mainly when raising to 1.3. What it does not give you: it is a *lower bound on your own side only*. The linked OpenSSL build and the platform crypto policy may impose a higher floor regardless, older versions may be unavailable in the build, and the version says nothing about who you are talking to - that is certificate verification's job. Check what was actually negotiated with `ssl.SSLSocket.version()`.

code

python · 9 lines
python
import ssl

ctx = ssl.create_default_context()
print("default floor:", ctx.minimum_version.name)

ctx.minimum_version = ssl.TLSVersion.TLSv1_3
print("raised to:", ctx.minimum_version.name)
print("this build offers TLS 1.3:", ssl.HAS_TLSv1_3)
print("linked library:", ssl.OPENSSL_VERSION)

go deeper

for a junior

Know that the protocol version floor is one attribute assignment - minimum_version set to an ssl.TLSVersion member - and that the default context already refuses TLS 1.0 and 1.1, so you rarely need to touch it.

for a middle

Explain the enum, why the version range replaced the per-version option flags, and that the setting bounds your own side only. Be able to say what ssl.SSLSocket.version() reports and why it can differ from what you configured.

for a senior

Show the operational judgement: raising the floor to TLS 1.3 breaks unupgraded peers at connection time, so measure the negotiated versions in real traffic first, and know that the platform crypto policy can already be stricter than your context.

for a principal

Own the policy across the estate - which clients may require 1.3, how deprecations are staged and communicated, and how you keep the floor rising through interpreter upgrades rather than through settings copied into every service.

## The API An `ssl.SSLContext` exposes `minimum_version` and `maximum_version`, both taking members of the `ssl.TLSVersion` enum: `MINIMUM_SUPPORTED`, `SSLv3`, `TLSv1`, `TLSv1_1`, `TLSv1_2`, `TLSv1_3`, `MAXIMUM_SUPPORTED`. Raising the floor is one assignment: ```python import ssl ctx = ssl.create_default_context() ctx.minimum_version = ssl.TLSVersion.TLSv1_3 ``` They are settable only on a general-purpose context - one created with `ssl.PROTOCOL_TLS_CLIENT` or `ssl.PROTOCOL_TLS_SERVER`, which includes everything `ssl.create_default_context()` returns. On a context pinned to one specific protocol there is nothing to bound and the assignment raises. This pair replaced an older idiom: setting bits such as `ssl.OP_NO_TLSv1_1` in `ssl.SSLContext.options`. Those flags still exist but are deprecated in favour of the version range, and there is a real reason to prefer the range beyond tidiness - an option-flag list enumerates the versions you knew about when you wrote it, so a future protocol version is permitted by omission, whereas `minimum_version` expresses the intent ("nothing older than this") and keeps expressing it. ## What the floor is worth, and what it is not A minimum version is a statement about *protocol mechanics*: it refuses handshakes that would use a version with known weaknesses. Three things it does not do, and all three are asked as follow-ups. **It is not authentication.** A TLS 1.3 connection to an attacker is still a connection to an attacker. Version policy and certificate verification are orthogonal, and the version is the far less important of the two - a verified TLS 1.2 connection is enormously safer than an unverified TLS 1.3 one. **It is not the only floor in play.** The linked OpenSSL build has its own default security level, and many platforms ship a system-wide crypto policy that raises the floor beneath the interpreter. A context that permits TLS 1.0 may still fail to negotiate it, because the library below refused before Python was consulted. The converse also holds: reading a permissive `minimum_version` off a context does not prove old protocols are reachable. Where support genuinely matters, `ssl.HAS_TLSv1_3` reports whether the build offers 1.3 at all, and `ssl.OPENSSL_VERSION` tells you which library you are actually on. **It is not observable from the setting.** What you configured is a bound; what happened is a negotiation. `ssl.SSLSocket.version()` on a connected socket returns the version actually agreed, and that is the value worth logging on a service you operate. ## Raising to TLS 1.3 is a compatibility decision Since 3.10, `ssl.create_default_context()` sets `minimum_version` to `ssl.TLSVersion.TLSv1_2`, so TLS 1.0 and 1.1 are already gone by default - the assignment people actually write is the one that moves the floor to 1.3. That is a policy decision with an operational cost, not a free hardening: any endpoint that has not been upgraded stops working, and it stops working as a handshake failure at connection time rather than as a warning. On a client that talks to a known, controlled set of internal endpoints, raising to 1.3 is reasonable and easy to verify. On a client that talks to arbitrary third parties, it will eventually hit a peer that cannot do 1.3, and the failure will be someone else's server on a Sunday. Decide per client, not globally, and roll it out by first logging `ssl.SSLSocket.version()` from real traffic to see what your peers actually negotiate. ## maximum_version is almost always the wrong knob `maximum_version` exists for interoperability testing and for reproducing a peer's behaviour. Setting it in production code is a mistake in waiting: it caps you below whatever comes next, and the usual motive - "a middlebox chokes on 1.3" - is a request to make your traffic worse in order to keep a broken device happy. If you find one set in a codebase, the question to ask is what it was working around and whether that thing still exists. ## Putting it together A defensible client setup is small: build the context with `ssl.create_default_context()`, leave verification alone because it is already right, raise `minimum_version` only when you own both ends and have checked they can do it, and log the negotiated version so the policy is measured rather than assumed. The floor is worth setting; it is not what makes the connection safe.

  • Why prefer minimum_version over setting option flags like ssl.OP_NO_TLSv1_1?
    The option flags are a list of versions you happened to know about, so anything newer is permitted by omission and the list has to be revisited. `minimum_version` states the intent - nothing older than this - and keeps holding as new versions appear. The flags are deprecated for exactly that reason.
  • Your context permits TLS 1.0 but the handshake still fails. What is going on?
    Something below Python refused first. The linked OpenSSL build has its own security level, and many platforms apply a system-wide crypto policy that removes old protocols and weak signature algorithms regardless of what the process asks for. The context's `minimum_version` is a lower bound on your side, not a guarantee that anything at that level is reachable.
  • How do you confirm what a running client is actually negotiating?
    Call `version()` on the connected `ssl.SSLSocket` - it returns the protocol version actually agreed, such as TLSv1.3. Logging that from real traffic is how you find out whether raising the floor would break peers before you raise it, rather than after.

saying these in an interview costs you the question

  • Treats a high TLS version as a substitute for certificate verification
  • Pins maximum_version in production to placate a middlebox
  • Still disables old protocols with individual option flags
  • Assumes the configured minimum is what got negotiated
  • Raises the floor to TLS 1.3 without checking what peers support

context