Behind a reverse proxy¶
Configure BlackBull's trusted proxies and use the examples below to connect nginx, HAProxy, or Envoy to its HTTP/1.1 or HTTP/2 listener.
Common setup — the BlackBull side¶
Start BlackBull without TLS. The proxy handles certificates:
python app.py --port 8000 # plain HTTP; no --cert / --key
Trusted-proxy headers¶
By default conn.client is the raw TCP peer — the proxy's address — and
conn.scheme is 'http' even when the client connected over HTTPS. Enable
TrustedProxy to recover both:
# Shortcut on BlackBull — registers TrustedProxy automatically
app = BlackBull(trusted_proxies=['127.0.0.1', '::1'])
# Or register explicitly for more control (e.g. a private subnet):
from blackbull import TrustedProxy
app.use(TrustedProxy(['127.0.0.1', '::1', '10.0.0.0/8']))
Set trusted_proxies to the proxies' actual source addresses; other peers'
forwarding headers are ignored. Each trusted proxy must append its observed peer
or overwrite the address chain.
conn.client identifies the nearest untrusted hop, which may itself be a proxy.
BlackBull prefers Forwarded over X-Forwarded-For and X-Forwarded-Proto.
The XFF-based examples below therefore remove incoming Forwarded and overwrite
X-Forwarded-Proto. They also remove X-Forwarded-Prefix; if mounting under
/api, overwrite it with /api instead. Proto and prefix must each be a single
value, not a list accumulated across proxies.
nginx¶
Terminates TLS and HTTP/2 toward clients, proxies to BlackBull over HTTP/1.1. Regular requests, WebSocket upgrades, and Server-Sent Events all work.
upstream blackbull {
server 127.0.0.1:8000;
keepalive 64;
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# ── Regular HTTP requests ─────────────────────────────────────────
location / {
proxy_pass http://blackbull;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Forwarded "";
proxy_set_header X-Forwarded-Prefix "";
proxy_set_header Connection ""; # enable keep-alive upstream
}
# ── WebSocket ─────────────────────────────────────────────────────
# WebSocket requires HTTP/1.1 Upgrade; match paths that need it explicitly.
location /ws {
proxy_pass http://blackbull;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Forwarded "";
proxy_set_header X-Forwarded-Prefix "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # keep WS connection open
}
# ── Server-Sent Events ────────────────────────────────────────────
location /sse {
proxy_pass http://blackbull;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Forwarded "";
proxy_set_header X-Forwarded-Prefix "";
proxy_set_header Connection "";
proxy_buffering off; # flush SSE events immediately
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding on;
}
}
# Redirect plain HTTP to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
HAProxy¶
HTTP/1.1 backend¶
global
log stdout format raw local0
defaults
mode http
timeout connect 5s
timeout client 60s
timeout server 60s
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
bind *:80
http-request redirect scheme https unless { ssl_fc }
http-request del-header Forwarded
http-request del-header X-Forwarded-Prefix
# Standalone metadata must replace client-supplied values.
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Forwarded-Proto http unless { ssl_fc }
option forwardfor # appends X-Forwarded-For
default_backend blackbull
backend blackbull
# WebSocket needs no special handling: HAProxy tunnels the Upgrade
# automatically in HTTP mode. Raise the server timeout for long-lived
# connections (WebSocket, SSE).
timeout tunnel 3600s
server bb1 127.0.0.1:8000 check
HTTP/2 backend¶
Change one thing — the server line. proto h2 forces cleartext HTTP/2
(h2c) to the backend:
backend blackbull
timeout tunnel 3600s
server bb1 127.0.0.1:8000 proto h2 check
Over TLS to the backend, negotiate it with ALPN instead:
server bb1 10.0.0.5:8443 ssl alpn h2 verify required \
ca-file /etc/haproxy/certs/ca.pem check
WebSocket over an HTTP/2 backend
WebSocket over HTTP/2 uses Extended CONNECT (RFC 8441), which is a
different mechanism from the HTTP/1.1 Upgrade handshake. BlackBull
supports it, but if you hit trouble, route WebSocket paths to an
HTTP/1.1 backend and keep HTTP/2 for the rest:
frontend https_in
acl is_ws path_beg /ws
use_backend blackbull_h1 if is_ws
default_backend blackbull_h2
Envoy¶
HTTP/1.1 backend¶
static_resources:
listeners:
- name: https_listener
address:
socket_address: {address: 0.0.0.0, port_value: 443}
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
use_remote_address: true # appends the observed peer to XFF
xff_num_trusted_hops: 0 # overwrites client-supplied proto
upgrade_configs:
- upgrade_type: websocket # required for WebSocket
route_config:
request_headers_to_remove: [forwarded, x-forwarded-prefix]
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match: {prefix: "/"}
route:
cluster: blackbull
timeout: 0s # no timeout: SSE / streaming
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
alpn_protocols: ["h2", "http/1.1"]
tls_certificates:
- certificate_chain: {filename: "/etc/envoy/certs/fullchain.pem"}
private_key: {filename: "/etc/envoy/certs/privkey.pem"}
clusters:
- name: blackbull
connect_timeout: 5s
type: STATIC
load_assignment:
cluster_name: blackbull
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: {address: 127.0.0.1, port_value: 8000}
HTTP/2 backend¶
Add typed_extension_protocol_options to the cluster. Everything else
stays as above:
clusters:
- name: blackbull
connect_timeout: 5s
type: STATIC
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {} # h2c to the backend
load_assignment:
cluster_name: blackbull
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: {address: 127.0.0.1, port_value: 8000}
For gRPC, this is the configuration you want — Envoy will multiplex all RPCs onto one upstream connection. See gRPC.
Docker¶
In Compose, the proxy reaches BlackBull by service name, so the trusted set must cover the Docker network rather than loopback:
app = BlackBull(trusted_proxies=['172.16.0.0/12'])
Going one step further — AF_UNIX¶
When the proxy and BlackBull share a host, an AF_UNIX socket between them
removes TCP overhead and avoids exposing a port on 0.0.0.0. See
Unix and fd inheritance.
# nginx
upstream blackbull { server unix:/run/blackbull.sock; }
# HAProxy
backend blackbull
server bb1 /run/blackbull.sock
# Envoy — in the cluster's endpoint address
address:
pipe: {path: /run/blackbull.sock}
With a Unix socket there is no meaningful IP to trust, so TrustedProxy needs
the peer that the socket presents rather than a network range — verify what
conn.client reports before assuming a value.
Choosing the upstream protocol¶
A proxy can terminate TLS and HTTP/2 and use HTTP/1.1 upstream. Choose an upstream protocol that preserves the application's required features: gRPC and multiplexed streams require HTTP/2 support along their path.
Next¶
- Workers — multi-worker is a natural fit behind a proxy; each worker can saturate one core while the proxy load-balances.
- Unix and fd inheritance — the
AF_UNIXbind pattern. - TLS — if you decide to terminate TLS in BlackBull instead.
- HTTP/2 — what the native HTTP/2 support gives you.