Python TLS/SSL Basics: HTTPS and Secure Connections
TLS (Transport Layer Security) is the protocol that encrypts data in transit over networks. HTTPS is HTTP over TLS. Every Python application that makes network requests or accepts incoming connections must use TLS to prevent eavesdropping, tampering, and man-in-the-middle attacks. In 2026, any connection not using TLS is considered insecure.
Understanding TLS is essential: how the handshake works, why certificate validation is critical, and how to implement secure sockets in Python. A common vulnerability is accepting self-signed certificates or disabling certificate verification—mistakes that negate TLS's protection.
How TLS Works: The Handshake
TLS is a two-phase protocol:
- Handshake (once per connection): Client and server authenticate each other and agree on an encryption key.
- Data transfer (multiple messages): Client and server exchange encrypted application data.
The handshake uses public-key cryptography (RSA, ECDHE) to securely exchange a symmetric encryption key. After the handshake, all data is encrypted with that symmetric key (fast bulk encryption).
Client Server
| |
|---- ClientHello ------------->| (cipher suites, TLS version)
| |
|<---- ServerHello -------------| (chosen cipher suite)
|<---- Certificate -------------| (server's public key in a cert)
|<---- ServerKeyExchange ------| (ECDHE parameters)
|<---- ServerHelloDone ---------|
| |
|---- ClientKeyExchange ------->| (client's ECDHE parameters)
|---- ChangeCipherSpec ------->| (switch to encrypted)
|---- Finished ------------------>| (encrypted handshake message)
| |
|<---- ChangeCipherSpec --------|
|<---- Finished <---------------| (encrypted handshake message)
| |
|===== ENCRYPTED DATA =========>| (application data, encrypted)
| |
The server's certificate contains its public key and is signed by a Certificate Authority (CA). The client verifies the CA's signature to ensure the server's identity is legitimate.
TLS Versions: Use 1.2 or 1.3
TLS has evolved through versions:
| Version | Year | Status | Use in 2026 |
|---|---|---|---|
| SSL 3.0 | 1996 | Deprecated (broken) | Never |
| TLS 1.0 | 1999 | Deprecated (weak) | Never |
| TLS 1.1 | 2006 | Deprecated (weak) | Legacy only |
| TLS 1.2 | 2008 | Current standard | Yes, widely supported |
| TLS 1.3 | 2018 | Modern, recommended | Yes, preferred |
In 2026, use TLS 1.2 or 1.3 exclusively. TLS 1.3 is faster (1-RTT handshake) and more secure (removed weak algorithms). Disable TLS 1.0, 1.1 to prevent downgrade attacks.
Making HTTPS Requests in Python
Python's requests library handles TLS automatically:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
# Simple HTTPS request (TLS is handled automatically)
response = requests.get("https://api.example.com/users")
print(response.json())
# By default, requests verifies the server's certificate
# If certificate verification fails, an exception is raised
try:
response = requests.get("https://self-signed.example.com")
except requests.exceptions.SSLError as e:
print(f"Certificate verification failed: {e}")
The requests library uses the system's CA certificate store to verify the server's certificate. This is secure and recommended.
Certificate Pinning: Extra Security
Certificate pinning ensures the application only communicates with a server using a specific certificate (or public key). This prevents attacks where a compromised CA issues a fraudulent certificate for your domain.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
import ssl
# Certificate pinning using public-key pinning (HPKP)
class HTTPSAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
context = create_urllib3_context(ssl_version=ssl.PROTOCOL_TLSv1_3)
context.check_hostname = True
context.verify_mode = ssl.CERT_REQUIRED
# Pin the server's public key (SHA-256 of the SPKI encoded public key)
# For "api.example.com", the pinned key is:
pinned_keys = [
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" # Example pin
]
# Note: urllib3 doesn't directly support HPKP, so in production use:
# - Certifi for automatic CA verification (default)
# - Custom verification logic (for pinning)
# - A library like pyOpenSSL with custom CA bundles
kwargs["ssl_context"] = context
return super().init_poolmanager(*args, **kwargs)
session = requests.Session()
session.mount("https://", HTTPSAdapter())
response = session.get("https://api.example.com/data")
For production pinning, use a dedicated library or implement custom verification.
Creating a Secure Server with TLS
A Python server using Flask or FastAPI should accept HTTPS connections:
from flask import Flask
import ssl
app = Flask(__name__)
@app.route("/api/data")
def get_data():
return {"message": "Encrypted connection!"}
if __name__ == "__main__":
# Create an SSL context with TLS 1.2/1.3
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(
certfile="server_cert.pem",
keyfile="server_key.pem"
)
# Enforce strong ciphers and TLS versions
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
context.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM")
# Run server with TLS
app.run(
host="0.0.0.0",
port=443,
ssl_context=context,
debug=False
)
The server loads a certificate (certificate chain) and private key from PEM files. These are obtained from a Certificate Authority (Let's Encrypt, Digicert, etc.) during domain registration.
Verifying Server Certificates Correctly
A common mistake is disabling certificate verification to avoid "untrusted certificate" errors:
# WRONG: Disables certificate verification (completely insecure)
response = requests.get("https://api.example.com", verify=False)
# WRONG: Uses a custom CA bundle but loosely (still risky)
response = requests.get("https://api.example.com", verify="/path/to/ca-bundle.crt")
Never set verify=False in production. This allows man-in-the-middle attacks. If the server uses a self-signed certificate (not recommended), verify it through an out-of-band channel before hardcoding it.
Correct approaches:
# CORRECT: Verify using the system's CA store (default and recommended)
response = requests.get("https://api.example.com")
# CORRECT: Verify using a custom CA bundle (for private CAs)
response = requests.get(
"https://internal.example.com",
verify="/path/to/custom-ca.crt"
)
# CORRECT: Verify and pin the public key (extra security)
# Implement custom verification logic or use a dedicated library
Generating Self-Signed Certificates for Testing
For development and testing, generate a self-signed certificate:
# Generate a private key
openssl genrsa -out server_key.pem 2048
# Generate a self-signed certificate (valid for 365 days)
openssl req -new -x509 -key server_key.pem -out server_cert.pem -days 365 -subj "/CN=localhost"
Important: Self-signed certificates are never used in production. Production servers use certificates issued by trusted CAs. Self-signed certificates are for:
- Local development
- Testing environments
- Internal network services (if you control the clients)
In all cases, clients must be explicitly configured to trust the self-signed certificate (via verify="/path/to/cert.pem").
Secure Socket Options
When creating raw sockets with TLS, configure security options:
import ssl
import socket
# Create a secure SSL context
context = ssl.create_default_context()
# TLS version constraints (enforce minimum)
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.maximum_version = ssl.TLSVersion.TLSv1_3
# Enable certificate verification
context.check_hostname = True
context.verify_mode = ssl.CERT_REQUIRED
# Load custom CA certificate (if needed)
# context.load_verify_locations("/path/to/ca-bundle.crt")
# Connect to a server
with socket.create_connection(("api.example.com", 443)) as sock:
with context.wrap_socket(sock, server_hostname="api.example.com") as ssock:
# Send encrypted data
ssock.sendall(b"GET / HTTP/1.0\r\nHost: api.example.com\r\n\r\n")
data = ssock.recv(1024)
print(data)
HSTS: Enforce HTTPS at the Protocol Level
HTTP Strict-Transport-Security (HSTS) is a header that tells browsers to always use HTTPS for your domain:
from flask import Flask
app = Flask(__name__)
@app.after_request
def set_hsts_header(response):
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
return response
@app.route("/")
def home():
return "Secure!"
The header tells the browser: "For the next year, always connect to this domain over HTTPS, even if the user types 'http://'.'' This prevents downgrade attacks and cookie theft.
Key Takeaways
- TLS (Transport Layer Security) encrypts data in transit; HTTPS is HTTP over TLS. Every production connection must use TLS 1.2 or 1.3.
- The TLS handshake uses public-key cryptography (RSA, ECDHE) to securely negotiate a symmetric encryption key; subsequent data is encrypted with that key (fast).
- Always verify server certificates against a trusted CA. Never disable verification (
verify=False). Self-signed certificates are for development only. - Certificate pinning (pinning to a specific certificate or public key) provides extra protection against compromised CAs.
- HSTS headers enforce HTTPS at the protocol level, preventing downgrade attacks.
Frequently Asked Questions
What is the difference between a certificate and a key?
A certificate contains a public key, identity information (domain name, organization), and a signature from a CA proving its authenticity. A key is the cryptographic secret used for encryption/decryption or signing. A certificate is public; a key is secret.
Why do I get a certificate verification error?
Common causes: the server's certificate is self-signed (not issued by a trusted CA), the domain name in the certificate doesn't match the server you're connecting to, or the certificate has expired. Solutions: use a trusted CA certificate, verify the domain name, renew expired certificates.
Can I use the same certificate for multiple domains?
Yes, using a wildcard certificate (e.g., *.example.com) or a Subject Alternative Name (SAN) certificate listing multiple domains. Let's Encrypt offers free SAN certificates.
Should I upgrade to TLS 1.3 immediately?
Yes. TLS 1.3 is faster (fewer round trips), more secure (removed weak algorithms), and widely supported. Most modern libraries and servers support it by default.
How do I renew a certificate before it expires?
Use the same CA that issued the original certificate. Most CAs provide renewal processes (manual or automated via ACME). Let's Encrypt and other CAs support automatic renewal via tools like Certbot.