Put a proxy in front of Kraft
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 theirOrigin, 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:
- First set a password, as in Set a password.
- 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 ownX-Forwarded-Forthrough as it came: a caller who sendsX-Forwarded-For: 127.0.0.1would then be taken for a client on this machine, and Kraft would ask it for no login. - Make the proxy send
Host: localhost, with the setting for yours.
| Proxy | Setting |
|---|---|
| cloudflared | httpHostHeader: localhost in the ingress rule's originRequest, or --http-host-header localhost on the command line |
| ngrok | --host-header=rewrite |
| Caddy | header_up Host localhost in the reverse_proxy block |
| nginx | proxy_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.
/api/healthanswers with a JSON status. A403unexpected Hostmeans the proxy still sends the name its client used./api/work-items, with no credentials, answers401. A403means no password is set: Kraft refuses every request through the proxy until one is (step 1 above).
A200means the proxy is not sendingX-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 withX-Forwarded-For: 127.0.0.1added: it must still answer401, and a200means 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/triggersfrom off-machine with the trigger token - Security: the threat model behind the password and the host check
Remote access
Approve or reject a gate from a phone, over Tailscale, a tunnel or a reverse proxy you run, using the same password auth as the local board, get back if Kraft does not restart, and rotate the token.
Restart Kraft without ending running agents
Pause the items that have an agent running, restart the server, and resume them, so the restart ends no agent.