Skip to main content
Version: 1.1

Reverse proxy

Fadebox's control plane serves plain HTTP and expects a TLS-terminating proxy in front of it. The Compose file in Installation publishes 8080 and leaves the proxy to you.

This is the control plane — the URL you sign in to. TLS for deployed instance URLs is a separate layer with its own edge proxy, already handled by the ingress bundle.

What the proxy has to send​

X-Forwarded-Proto is the header that matters. Everything Fadebox says about where it lives is built from the request as it arrives, so without it the server believes it is plain HTTP:

  • the session cookie is marked Secure only when the request looks like https, and a cookie without that flag is one the browser will also send over plain HTTP;
  • sign-in redirects are absolute, so a wrong scheme bounces a user back out of https;
  • the audit log's client address comes from X-Forwarded-For — without it every event records the proxy's own address, the same value for every user;
  • the OAuth document for MCP must advertise the exact URL the client dialled, and a client that connected over https rejects an endpoint that calls itself http.

Send all three: X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-For.

X-Forwarded-Host is the one you may not actually need. Most proxies pass the browser's Host through untouched, and that is what Fadebox uses when the header is absent — the two agree, so sending it changes nothing. It is honoured because some setups replace Host with the backend's name (a few proxy_pass configurations, some path-based routing, some cloud load balancers), and Fadebox would otherwise advertise its own container address back to the browser.

:::warning Strip what the client sent

Fadebox reads the leftmost value of X-Forwarded-For. A proxy that appends to the header rather than replacing it therefore lets any caller choose the address written to the audit log, just by sending an X-Forwarded-For of their own. Delete the inbound forwarding headers at the edge before adding your own — the blueprints below do.

:::

Traefik​

Traefik sets all three headers itself; there is nothing to configure for them. The usual case is that Traefik is already running on the host — it is what the ingress bundle installs — so the control plane joins the same edge network and asks for a router:

services:
app:
image: fadebox/fadebox:1.1
# No `ports:` — with Traefik in front, publishing 8080 is exactly what makes the
# forwarding headers forgeable by anything else on the host.
environment:
# …the variables from Installation…
QUARKUS_HTTP_PROXY_TRUSTED_PROXIES: 172.16.0.0/12
networks: [default, fadebox-edge]
labels:
- traefik.enable=true
- traefik.docker.network=fadebox-edge
- traefik.http.routers.fadebox.rule=Host(`fadebox.example.com`)
- traefik.http.routers.fadebox.entrypoints=websecure
- traefik.http.services.fadebox.loadbalancer.server.port=8080

networks:
fadebox-edge:
external: true

Three things worth knowing:

  • traefik.enable=true is required, because the bundle runs Traefik with providers.docker.exposedbydefault=false — only containers that ask for it become routable.
  • The certificate has to cover this host. The bundle's websecure entrypoint carries one wildcard for the runtime domain, so fadebox.example.com is covered only if it sits under that domain. Otherwise give the router its own resolver (traefik.http.routers.fadebox.tls.certresolver=le).
  • Leave forwardedHeaders.trustedIPs unset on the public entrypoint. Unset, Traefik overwrites the forwarding headers on every request, which is what you want; listing an address there tells Traefik to pass that peer's own headers through untouched.

Set QUARKUS_HTTP_PROXY_TRUSTED_PROXIES to the network Traefik reaches Fadebox on — docker network inspect fadebox-edge prints the subnet.

HAProxy​

HAProxy needs saying outright: option forwardfor sets X-Forwarded-For and nothing else, and it appends.

global
log stdout format raw local0

defaults
mode http
log global
option httplog
timeout connect 5s
timeout client 5m
timeout server 5m

frontend fadebox
bind :80
bind :443 ssl crt /etc/haproxy/certs/fadebox.example.com.pem alpn h2,http/1.1
http-request redirect scheme https unless { ssl_fc }

# Drop whatever the caller sent before adding our own. Fadebox reads the leftmost
# X-Forwarded-For, so an appended value would let a caller pick its own audit-log address.
http-request del-header X-Forwarded-For
http-request del-header X-Forwarded-Proto
http-request del-header X-Forwarded-Host

option forwardfor
# Everything past the redirect above arrived over TLS, so this needs no condition.
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Host %[req.hdr(host)]

default_backend fadebox_app

backend fadebox_app
server app 10.0.0.5:8080 check

set-header rather than add-header throughout, so a value a client supplied is replaced instead of appended to.

The five-minute timeout server is deliberate: the MCP endpoint at /mcp can hold a long-lived response stream, and the default one minute cuts it. If you front only the UI and the REST API, the default is fine.

Traefik and Caddy send all three from their usual proxy directives, and replace what the client sent. Nginx sends none of them: a bare proxy_pass forwards the request with no X-Forwarded-* at all, so they have to be spelled out — the distro snippets that appear to do it for you (proxy_params and friends) are just these lines in a file:

location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}

$remote_addr rather than $proxy_add_x_forwarded_for, so the value is the peer nginx actually sees instead of whatever the client sent with the peer appended — the same reason the HAProxy config deletes the inbound headers first.

Lock the trust down​

The same trust that makes the headers work makes them forgeable. QUARKUS_HTTP_PROXY_TRUSTED_PROXIES names the proxy's own address (IP or CIDR, comma-separated); while it is unset, every peer is trusted and anything that can reach the HTTP port by another route can choose the scheme and host Fadebox advertises and the client address it records. Fadebox logs a warning at startup until you set it.

Not publishing port 8080 at all is the other half of the same job, and the stronger one: a port nothing can reach directly cannot be addressed directly.

See Configuration reference for both proxy variables.

Checking it worked​

The sign-in challenge is an absolute URL built from the request, so it reports what the server believes about itself — no credentials needed:

curl -sI https://fadebox.example.com/api/users/me | grep -i '^location:'
location: https://fadebox.example.com/sign-in

An http:// scheme or the wrong host there means the headers are not arriving. Once that line is right, sign in and check the session cookie in your browser's devtools: quarkus-credential should be flagged Secure, HttpOnly and SameSite=Strict.