Production TLS and edge headers¶
Read this before exposing FluxLit on the public internet: where TLS terminates, how HSTS and forwarded headers interact with Uvicorn, and why Content-Security-Policy is usually an edge concern—not a one-line toggle for Streamlit.
For deployment basics (health probes, Docker, graceful shutdown), see Deployment. For env reference, see Configuration. For correlation and gateway logs, see Observability.
Terminate TLS where you test¶
Use the same termination model in staging as production (same ingress, same
X-Forwarded-*behavior, same certificate chain validation).The repository’s
docker/proxy-deployment/includes HTTPS Compose (docker-compose.https.yml) andgenerate-test-certs.shplussmoke-test.sh—use it to validate subpath routing and headers against real TLS, not only plain HTTP to localhost.
HSTS: edge vs application¶
FluxLit’s optional SecurityHeadersMiddleware (enable with FLUXLIT_ENABLE_SECURITY_HEADERS) sets Strict-Transport-Security when the request’s effective scheme is HTTPS (from X-Forwarded-Proto first value or the URL scheme). That only applies to responses served by this stack.
If your CDN or ingress already sends HSTS, avoid duplicating conflicting directives (different
max-ageorincludeSubDomains). Prefer one owner—usually the outermost TLS terminator.If TLS terminates only at the edge, the app must see
X-Forwarded-Proto: https(see trust settings below) or it will not emit HSTS from the middleware path.
Trusting proxies: forwarded_allow_ips¶
When FLUXLIT_TRUST_PROXY is on (or fluxlit run --proxy-headers), Uvicorn trusts X-Forwarded-* for client IP and scheme. FLUXLIT_FORWARDED_ALLOW_IPS is forwarded to Uvicorn as forwarded_allow_ips.
Default when trusting proxy and unset:
*(trust all connecting IPs). That is convenient in dev and risky if clients can reach the app without going through your proxy.Production: set
FLUXLIT_FORWARDED_ALLOW_IPSto your load balancer / ingress CIDRs or link-local ranges your mesh uses—see Uvicorn settings for the exact string format (comma-separated IPs or networks).
nginx: real client IP and forwarded scheme¶
Your proxy must set X-Forwarded-Proto (and typically X-Forwarded-For) from trusted variables (e.g. $scheme from the TLS listener), not from user-controlled input.
Example patterns (conceptual—adapt to your docker/proxy-deployment layout):
# Trust only your edge/load balancer when using real_ip (example)
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
The repo’s docker/proxy-deployment/nginx.conf is the maintained reference for strip-prefix and subpath smoke tests.
Path-prefixed mount (/apps/my-app)¶
Many platforms expose apps under a namespace path (for example /apps/<name>/ on a shared host). FluxLit needs three things to line up: the browser-visible prefix (FLUXLIT_ROOT_PATH), proxy trust so scheme/host come from your edge (FLUXLIT_TRUST_PROXY / FLUXLIT_FORWARDED_ALLOW_IPS), and—when you use OAuth or absolute links—FLUXLIT_PUBLIC_BASE_URL pointing at the same origin + path users open.
nginx: HTTP + WebSockets (strip prefix)¶
Forward /apps/my-app/ to the gateway and strip that prefix before it reaches Uvicorn (same pattern as docker/proxy-deployment/nginx.conf, which uses /myapp/). WebSockets use Upgrade / Connection and a long proxy_read_timeout because Streamlit keeps /_stcore/stream open.
Maintained reference for the /apps/my-app/ shape: docker/proxy-deployment/nginx-apps-prefix.conf. Bring it up with:
docker compose -f docker/proxy-deployment/docker-compose.yml \
-f docker/proxy-deployment/docker-compose.apps-prefix.yml up --build
Environment (container or process)¶
Variable |
Example |
Role |
|---|---|---|
|
|
Must match the path segment users type after the host (no trailing slash). |
|
|
Lets Uvicorn honor |
|
Your LB CIDRs |
Not |
|
|
OAuth redirect base and stable absolute links; include the path when the app is not at |
TLS usually terminates at nginx or an ingress in front of FluxLit. Set X-Forwarded-Proto: https from the TLS listener (or $scheme on an HTTPS server block), not from client-controlled input.
Generated URL shapes (browser → gateway)¶
Assume public origin https://portal.example.com and mount /apps/my-app (strip-prefix proxy). Typical URLs:
User-facing URL |
Served by |
|---|---|
|
Streamlit shell (HTML + |
|
Swagger UI (FastAPI OpenAPI) |
|
Liveness JSON |
|
Readiness JSON |
|
Streamlit WebSocket (subprotocol |
In Python, prefer FluxLit.urls (urls) so links follow FLUXLIT_ROOT_PATH and forwarded scheme/host; see Configuration (“Public URL helpers”).
TLS termination and trust¶
Terminate TLS where you run your certificates (ingress, CDN, nginx). FluxLit still sees plain HTTP on the container port; it must receive
X-Forwarded-Proto: httpswhen you enableFLUXLIT_ENABLE_SECURITY_HEADERSso HSTS is only sent over HTTPS.Forwarded IP trust: If
FLUXLIT_TRUST_PROXYis on andFLUXLIT_FORWARDED_ALLOW_IPSis unset, Uvicorn defaults to*(trust every peer). Tighten to your reverse-proxy IP ranges so clients cannot spoofX-Forwarded-Forfrom inside the trusted hop.
Troubleshooting with fluxlit doctor and debug¶
Run
fluxlit doctor app:app(or your import path) inside the same image/env as production. Inspect WARN/FAIL rows forforwarded_allow_ips,public_base_url,proxy_headers,import_shadowing,gateway_forward_rejected_names(when the allowlist names credential-style headers that are never forwarded), and relatedfluxlit configwarnings you can reproduce withfluxlit config app:app --json.With
FLUXLIT_DEBUG=1, useGET …/__fluxlit/debug(when not shadowed byapi_mount_path) for a redacted view of effective settings and recent gateway dispatches—see Configuration and Troubleshooting.If
GET …/api/readyzis 503, Streamlit is not reachable from the gateway; see Runbooks and Deployment readiness notes.
CI runs docker/proxy-deployment/run-all-proxy-smokes.sh, including the /apps/my-app strip-prefix stack on port 8083, plus smoke-test.sh checks for /api/docs and WebSockets.
Content-Security-Policy (CSP) and Streamlit¶
Streamlit serves dynamic scripts, WebSockets, and assets that evolve with versions. A strict CSP copied from a generic SPA guide will often break the UI (inline script hashes drift, connect-src for websockets, frame-ancestors for embedding).
Practical guidance:
Treat CSP as optional for the Streamlit-heavy routes unless you have tested every page with your Streamlit version.
Prefer tight CSP on separate static sites (marketing, docs) and minimal, deliberate directives on
/apiif you add middleware yourself.FluxLit’s built-in middleware does not set CSP today; adding a global CSP belongs in your ingress or a carefully scoped FastAPI middleware after testing.
Validate TLS end-to-end (quick runbook)¶
Build and start the HTTPS stack under
docker/proxy-deployment/(see that directory’s README).From the host, call through the TLS listener (see
BASE_URLin smoke scripts), e.g.:CURL_INSECURE=1 curl -fsSI https://127.0.0.1:8444/myapp/api/healthz
Use your real CA in production instead of
CURL_INSECURE.Confirm
Strict-Transport-Securityappears when the app believes the request is HTTPS (directly or viaX-Forwarded-Proto).Confirm
GET /api/readyzreturns 200 when Streamlit is configured and healthy.
Read-only root filesystem (Kubernetes)¶
Enabling securityContext.readOnlyRootFilesystem: true is compatible in principle but requires writable mounts for anything the process must write at runtime (for example TMPDIR, Streamlit cache directories under the user home, pidfile paths if used, and uploaded temp files). Start with a tmpfs mount for /tmp and an emptyDir for a dedicated cache path; validate with your exact image and fluxlit run flags before rolling out cluster-wide.
Multi-engine ingress (with deployment)¶
For affinity, WebSocket upgrades on /_stcore/stream, and strip-prefix alignment with FLUXLIT_ROOT_PATH, see the Ingress engines table in Deployment (nginx, Traefik, Caddy pointers into docker/proxy-deployment/).