Skip to content

TLS

BlackBull terminates TLS itself when you pass certfile and keyfile. ALPN negotiates HTTP/2 (h2) or HTTP/1.1 automatically on the same listening port — there's no separate HTTP/2 port and no upgrade dance.

This page covers self-signed certs for local development, the ALPN behaviour you get for free, and mutual TLS (mTLS) when you need it.

Quick start

app.run(port=8443, certfile='cert.pem', keyfile='key.pem')

Or via the CLI:

blackbull myapp:app --bind 0.0.0.0:8443 \
    --certfile cert.pem --keyfile key.pem

That single listener serves:

  • HTTP/1.1 to any client that negotiates http/1.1 (or doesn't speak ALPN).
  • HTTP/2 to any client that negotiates h2 (modern browsers, curl --http2, httpx with http2=True, h2load).
  • WebSocket over either protocol — see WebSockets.

Self-signed certificate for local HTTPS

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
    -days 365 -nodes -subj '/CN=localhost' \
    -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1'

Then start the server:

python app.py --cert cert.pem --key key.pem --port 8443
# Open https://localhost:8443  (accept the browser security warning once)

Browsers and HTTP/2

Browsers only use HTTP/2 over HTTPS. With TLS the server negotiates HTTP/2 automatically via ALPN; plain HTTP connections use HTTP/1.1.

h2c — cleartext HTTP/2

For deployments where TLS is terminated upstream (typical in private networks behind a load balancer), BlackBull also detects HTTP/2 over cleartext via the connection preface — no upgrade dance:

python app.py --port 8000     # serves both HTTP/1.1 and h2c on the same socket

Modern HTTP clients that send the HTTP/2 preface directly get HTTP/2; everyone else gets HTTP/1.1.

Mutual TLS (mTLS)

mTLS requires clients to present a certificate signed by a trusted CA. Set it up before starting the server:

import asyncio
from blackbull import BlackBull
from blackbull.server import ASGIServer

app = BlackBull()

# ... define routes ...

# Build the server manually so we can configure mTLS before accepting connections.
server = ASGIServer(app, certfile='cert.pem', keyfile='key.pem')
server.open_socket(8443)
server.configure_mtls(ca_cert='ca.pem')   # enables CERT_REQUIRED
asyncio.run(server.run())

configure_mtls raises RuntimeError if called before TLS is configured (i.e. before certfile and keyfile are provided).

Generating a test CA and client certificate

# CA
openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.pem \
    -days 365 -nodes -subj '/CN=Test CA'

# Server cert signed by the CA
openssl req -newkey rsa:4096 -keyout key.pem -out server.csr \
    -nodes -subj '/CN=localhost'
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key \
    -CAcreateserial -out cert.pem -days 365

# Client cert signed by the CA
openssl req -newkey rsa:4096 -keyout client.key -out client.csr \
    -nodes -subj '/CN=client'
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key \
    -CAcreateserial -out client.pem -days 365

A client that doesn't present a valid certificate signed by the configured CA is rejected at the TLS handshake — your application code never sees the connection.

When to terminate TLS upstream instead

For most production deployments, terminating TLS at a reverse proxy (nginx, Caddy) is simpler:

  • Certificate rotation lives next to the proxy's existing certificate management.
  • The reverse proxy already buffers slow clients (mitigating slowloris before BlackBull sees the connection).
  • You can use AF_UNIX between proxy and BlackBull, removing TCP overhead.

See Behind a reverse proxy for the configuration on both sides, and Unix and fd inheritance for the UDS bind pattern.

Terminate TLS in BlackBull when:

  • You want HTTP/2 directly to the client without a proxy in the data path.
  • You need mTLS (clients presenting certificates).
  • You're running a single-process deployment where adding nginx isn't worth the operational complexity.

Next