v2.0.0next
Run the server

Put a proxy in front of Kraft

Let a tunnel or reverse proxy on this machine pass webhooks and scripts to a loopback-bound Kraft by sending Host localhost, and check that nobody gets in without signing in.

A tunnel or reverse proxy on this machine passes webhooks and scripts to a loopback-bound Kraft, and every API call through it signs in or presents a token.

Before you start

  • Kraft is bound to 127.0.0.1, which is the default.
  • A tunnel or reverse proxy on this machine: cloudflared, ngrok, Caddy or nginx.
  • A second device to check from. A request from this machine names a loopback client, and Kraft asks it for no password.

Choose the fix

A tunnel or reverse proxy on this machine that passes on the name its clients used gets a 403 for every request: unexpected Host for a server bound to 127.0.0.1. Scripts and webhooks get it too, and allowed_hosts is not read. Who may call has the rule. Coming from 1.4, see Network access.

Choose the fix by what has to reach Kraft:

  • The board: use Remote access instead, with a password and the name in allowed_hosts. For a proxy you run, such as Caddy or nginx, that is Behind a reverse proxy you run. A loopback-bound Kraft behind a proxy serves webhooks and scripts only, never the board: a browser's sign-in, every change it makes on the board and the live-update socket carry the name it used as their Origin, and Kraft refuses those as cross-site.
  • Webhooks and scripts only: have the proxy send Host: localhost, as below.

Send Host: localhost from the proxy

Before you rewrite Host, know what it does to sign-in. A loopback-bound Kraft asks a request from this machine for no password, and a request through a proxy on this machine is one, unless the proxy names its client in X-Forwarded-For. So:

  1. First set a password, as in Set a password.
  2. Make sure the proxy sets that header itself: nginx does not unless you add proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;, which appends the client's address. Appending or replacing is right. Never let the proxy pass a caller's own X-Forwarded-For through as it came: a caller who sends X-Forwarded-For: 127.0.0.1 would then be taken for a client on this machine, and Kraft would ask it for no login.
  3. Make the proxy send Host: localhost, with the setting for yours.
ProxySetting
cloudflaredhttpHostHeader: localhost in the ingress rule's originRequest, or --http-host-header localhost on the command line
ngrok--host-header=rewrite
Caddyheader_up Host localhost in the reverse_proxy block
nginxproxy_set_header Host localhost;

Kraft then asks every API call through the proxy for a session, for the mcp-token as a bearer, or, on POST /api/triggers, for the trigger token. /api/health, the page shell and its assets need none. While no password is set it refuses every request. Without X-Forwarded-For, everyone who reaches the proxy has the whole API, with no sign-in, password or not.

Kraft takes the client from the last X-Forwarded-For address that is not 127.0.0.1 or ::1. It reads the header only from a peer at one of those two addresses, which is uvicorn's default list. FORWARDED_ALLOW_IPS in the server's environment replaces that list (* trusts any peer). For the loopback-bound Kraft this page covers, keep it unset. A proxy on another machine needs it set to that proxy's address for a per-client lockout, as Behind a reverse proxy you run says.

A proxy that is itself fed from loopback, such as an ssh -R endpoint or a second proxy on this machine, passes every request on as local, and Kraft asks it for no password even with one set. Put nothing like that in front of a loopback-bound Kraft.

Check it worked

You know it worked when two requests through the proxy answer as follows. Make them from another device, not from this machine: a request from this machine names a loopback client, and Kraft asks it for no password.

  1. /api/health answers with a JSON status. A 403 unexpected Host means the proxy still sends the name its client used.
  2. /api/work-items, with no credentials, answers 401. A 403 means no password is set: Kraft refuses every request through the proxy until one is (step 1 above).
    A 200 means the proxy is not sending X-Forwarded-For, so Kraft takes every request for a local one and everyone who reaches the proxy has the whole API. Fix that before you go on. Then send it again with X-Forwarded-For: 127.0.0.1 added: it must still answer 401, and a 200 means the proxy passes a caller's own header through.

Next steps

  • Remote access: reach the board itself, over Tailscale, your local network or a tunnel
  • Who may call: the rule behind the 403
  • Inbound triggers: call POST /api/triggers from off-machine with the trigger token
  • Security: the threat model behind the password and the host check
Copyright © 2026