---
last_edited: 2026-08-29
---

# Remote daemon

A daemon can serve clients on other hosts over a private network. This is an
opt-in mode for trusted environments. It is not the same thing as a public
multi-tenant server.

## Server setup

Start the daemon on a literal non-public IP address:

```sh
KATA_AUTH_TOKEN=change-me KATA_TRUST_PRIVATE_NETWORK=1 \
  kata daemon start --foreground --listen 100.64.0.5:7777
```

Or configure it persistently in `<KATA_HOME>/config.toml`:

```toml
listen = "100.64.0.5:7777"

[auth]
token = "change-me"
trust_private_network = true
```

The CLI flag wins over config. Auto-started daemons also read the config-file
listener, so a host that should always use one TCP address only needs the config
set once.

Run the daemon under a process manager such as launchd, systemd, or a container
runtime on the host that owns the SQLite database.

## Runtime profiling

The daemon exposes Go's standard `net/http/pprof` handlers under
`/debug/pprof/` on its existing listener. The same listener and authentication
policy protect profiling requests; a token-protected TCP daemon requires its
normal bearer token.

Capture and inspect CPU and heap profiles with standard Go tools:

```sh
curl -H "Authorization: Bearer $KATA_AUTH_TOKEN" \
  'http://100.64.0.5:7777/debug/pprof/profile?seconds=30' \
  -o kata.cpu.pprof
go tool pprof kata.cpu.pprof

curl -H "Authorization: Bearer $KATA_AUTH_TOKEN" \
  'http://100.64.0.5:7777/debug/pprof/heap' \
  -o kata.heap.pprof
go tool pprof kata.heap.pprof
```

The index also links the goroutine, allocation, mutex-contention, blocking,
thread-creation, command-line, symbol, and execution-trace endpoints. Capture
`/debug/pprof/trace?seconds=5` and open it with `go tool trace`.

## Client setup

Point clients at the daemon:

```sh
export KATA_SERVER=http://100.64.0.5:7777
export KATA_AUTH_TOKEN=change-me
kata list
```

For a per-workspace, gitignored setting:

```toml
version = 1

[server]
url = "http://100.64.0.5:7777"
```

`KATA_SERVER` wins over `.kata.local.toml` unless a command passes
`--daemon <name>`.
If none of those are set, clients next honor `active_daemon` in
`<KATA_HOME>/config.toml`; otherwise they use local daemon discovery or
auto-start.

To inspect the endpoint selected by those rules, including its transport and
canonical request URL, run:

```sh
kata daemon locate --json
```

The command probes configured remotes without printing or resolving their
tokens. External clients should use this supported output instead of
reimplementing the selection order. See
[Daemon discovery](../reference/daemon-discovery.md) for the schema and Unix
socket address form.

## Browser access

Plain `kata ui` always starts or discovers the local browser gateway; it does
not use `KATA_SERVER`. The gateway initially selects `active_daemon` when one
is configured.

To open a remote daemon directly, add it as a named `[[daemon]]` entry and run
`kata ui --daemon <name>`. If the remote is configured only through
`KATA_SERVER`, open its canonical `/kata` route in a browser instead, for
example `https://daemon.example/kata`. A remote browser route shows an
in-document token login; the token is exchanged for a browser session in
memory and is never put in the URL or browser storage. Identity-token mode
keeps the token actor authoritative. Static-token deployments use the daemon's
normal request-actor policy.

For an HTTPS reverse proxy, set `[web].public_origin` to the exact browser
origin and route the UI, assets, `/api/v1/ui/*`, ordinary `/api/v1/*`, and the
event stream to the same daemon. Do not rewrite the browser application to a
different API origin or infer the public origin from forwarded headers.
When that browser listener is also a configured trusted-proxy listener, Kata
exchanges the proxy-asserted actor for a browser session automatically; it does
not ask the user for a daemon token.

When the shared TCP listener binds a wildcard address but clients use an
intentional backend hostname (for example, a container-network alias), add
that exact authority to `[web].allowed_hosts`. This is separate from
`public_origin`: it admits a backend request `Host` without declaring a new
browser origin. Keep the list narrow; bearer authentication does not bypass
Host validation.

An unauthenticated `--insecure-readonly` listener can display the browser UI,
but mutations are disabled and updates use polling. A user who authenticates
under a writable configuration receives the capabilities of that session.

## Plain HTTP guardrails

Mutable non-loopback HTTP requires one of:

- a daemon API token plus explicit trusted private-network opt-in;
- explicit unauthenticated private-network write opt-in on a literal private IP.

Plain HTTP bearer-token targets must be literal non-public IPs: loopback,
RFC1918, CGNAT, link-local, or ULA. Public IPs and DNS hostnames are rejected
for plaintext bearer auth. Use HTTPS through a reverse proxy or an SSH tunnel
for those shapes.

For private overlay hostnames where HTTPS is intentionally not used, clients can
opt out per target with `KATA_ALLOW_INSECURE=1` or
`[server].allow_insecure = true`. Federation hub enrollment tokens use their
own credential store, so spokes opt in with `kata federation join
--allow-insecure`.

```toml
version = 1

[server]
url = "http://hub.internal:7777"
allow_insecure = true
```

Unix sockets, loopback HTTP, and HTTPS do not require the same private-network
trust opt-in.

## Tokenless private-network writes

For a single-user private network where the network itself is the access
boundary:

```toml
listen = "100.64.0.5:7777"

[auth]
allow_unauthenticated_private_network_writes = true
```

or:

```sh
KATA_ALLOW_UNAUTHENTICATED_PRIVATE_NETWORK_WRITES=1 \
  kata daemon start --listen 100.64.0.5:7777
```

This moves kata's local no-token trust model onto the configured private IP:
mutations and the event stream are accepted without a bearer token, and write
actors come from the client request body just as they do on loopback and Unix
sockets. Any device that can reach the bound address can assert any actor string,
so use this only when tailnet or private-network membership is the intended
trust boundary.

The bind must be a literal private IP address: RFC1918, CGNAT, link-local, or
ULA. Hostnames, public IPs, and wildcard binds such as `0.0.0.0:7777` or
`:7777` are rejected for this mode. It cannot be combined with `token`,
`require_token_identity = true`, or `--insecure-readonly`.

Token administration remains authenticated-only in this mode. `/api/v1/tokens`
requests are rejected rather than letting unauthenticated private-network
clients mint, list, or revoke API tokens.

## Identity tokens

For stable per-user attribution, mint DB-backed tokens and enable identity
mode:

```sh
export KATA_AUTH_TOKEN=bootstrap-admin-token
kata tokens create --actor wesm --name laptop
kata tokens list
kata tokens revoke 1
```

Then configure:

```toml
[auth]
token = "bootstrap-admin-token"
trust_private_network = true
require_token_identity = true
```

In identity mode:

- the bootstrap token can create, list, and revoke user tokens;
- the bootstrap token can perform reads;
- attributed writes require a DB-backed token;
- the daemon derives the actor from the token and ignores body-provided actor
  strings for mutations.

Connector administration is denied to database-backed tokens by default.
Enable it only when every active identity token should administer connectors
and external-root bridges across the daemon. Add this setting to the existing
`[auth]` table:

```toml
allow_identity_connector_administration = true
```

The environment equivalent is
`KATA_ALLOW_IDENTITY_CONNECTOR_ADMINISTRATION=1`. The identity-mode bootstrap
token, browser sessions, and trusted-proxy principals still cannot administer
connectors.

The `kata federation enroll` CLI workflow also uses normal direct-client auth
when it talks to the hub. Run it with a DB-backed personal token; the generated
enrollment token printed by that command is separate and is only for spoke
transport. The enrollment is bound to the token actor in identity mode, and
the hub rejects pushed federation events whose actor differs from that bound
actor. See [Federation token boundaries](federation.md#token-boundaries).

Token lifecycle events are stored in the event log and preserved by backup,
restore, and JSONL cutover. Hidden system-project token events are excluded
from ordinary project lists, stats, event feeds, and federation.

## Web or OAuth frontends

A web app can sit in front of kata without daemon-side OAuth. The web app
should authenticate the browser user, map that identity to a kata actor handle,
and call the daemon with a server-side kata bearer token. The browser should
hold only the web app session, never the daemon token.

Because the daemon stores only token hashes, a web app cannot retrieve an
existing plaintext kata token later. It must vault plaintext tokens server-side
or mint and revoke tokens as part of its own session lifecycle.

## Trusted-proxy actor header

When a reverse proxy already authenticates users (through SSO, OAuth, or mutual
TLS), it can assert the kata actor on each request through a configured header,
and the daemon credits that actor instead of any client-supplied value. This is a
third attribution pattern alongside identity tokens (where the daemon is the auth
boundary, for direct clients holding personal tokens) and the web-frontend
pattern above: here the proxy is the auth boundary and the daemon owns only audit
attribution.

The mode is off by default and changes nothing until configured. Enable it by
naming the header and the listeners on which it is honored:

```toml
[auth.proxy]
trusted_actor_header = "X-Kata-Actor"
trusted_proxy_listeners = ["unix:///run/kata/proxy.sock"]
```

The environment overrides are `KATA_TRUSTED_ACTOR_HEADER` and
`KATA_TRUSTED_PROXY_LISTENERS` (comma-separated). An empty or unset env var means
"no override," so a mode enabled in `config.toml` cannot be silently turned off
by an empty environment variable; disabling it requires editing the config.

After merging environment and config:

- Header empty or absent: mode is off; any header is ignored everywhere.
- Header set but `trusted_proxy_listeners` empty: **rejected at config load**. A
  silent no-op here would let an operator believe proxy attribution is on while
  body-supplied actors keep flowing.
- Header and at least one listener set: on for those listeners; every other
  listener passes through unchanged.

### Listener addresses

Listener entries must be **literal** bind addresses that match the daemon's
`listen` value: a Unix socket path (`unix:///run/kata/proxy.sock`) or a specific
`host:port` (`100.64.0.5:7777`). Wildcard binds (`0.0.0.0:7777`, `:7777`) are
never valid entries: an accepted connection reports the specific interface it
arrived on, so a wildcard would never match. A trusted listener should be a Unix
socket or a private IP that only the proxy can reach.

### Security model

The header is trustworthy only because of where it is honored, so the deployment
must hold up its end:

- **Trust is bound to the listed listeners.** A client on any other path cannot
  set the header to spoof an actor. The header is meaningful exactly because
  nothing but the proxy can reach that listener.
- **The proxy must strip any client-supplied copy of the header** before
  forwarding, and must send a single value. The daemon uses the first value of
  the configured header and does not police duplicates; sanitizing inbound
  headers at the proxy boundary is the operator's responsibility.
- **Terminate each attribution mode on its own listener.** On a trusted listener
  the proxy-asserted actor overwrites a token identity, so running direct
  token-holding clients and the proxy on the same listener would discard their
  token identity. Give the proxy a Unix socket and direct clients a separate TCP
  port.
- **A proxy front-end cannot mint or revoke tokens.** Token-admin endpoints stay
  restricted to the bootstrap or loopback path regardless of the header; the
  proxy asserts identity, never token administration.

### Behavior

On a trusted listener the header value becomes the actor and any body or query
actor is ignored. A mutation that arrives on a trusted listener with the header
missing or empty is rejected with `400 actor_header_required` rather than falling
back to a client-supplied actor; otherwise a non-proxied client could omit the
header and claim any identity. Reads carry no actor, never reach actor
resolution, and are never blocked by this mode. The header asserts **identity,
not permission**: it does not add per-operation or per-actor authorization.

## Read-only private-network experiments

For unauthenticated experiments:

```sh
kata daemon start --foreground --listen 100.64.0.5:7777 --insecure-readonly
```

This permits GET requests only. Mutations and the event stream still require
authentication. Network ACLs, VPNs, or tailnet policy are the access boundary in
this development mode.

Clients of a plaintext private-network daemon must opt into the same trust the
server does, even when no token is configured:

```sh
export KATA_SERVER=http://100.64.0.5:7777
export KATA_TRUST_PRIVATE_NETWORK=1
```

kata validates the bearer target whenever it builds a client, so a plaintext
non-loopback endpoint is refused up front rather than failing later as an opaque
error. The opt-in states that the private network is the access boundary;
without it, use HTTPS, a loopback address, or an SSH tunnel.

`require_token_identity = true` cannot be combined with
`--insecure-readonly`.

## What this mode does not provide

Remote daemon mode is not a full authorization system. There is no project ACL
model, role model, impersonation scope, OAuth provider, or browser-safe daemon
token flow in the daemon itself. A reverse proxy can still own user
authentication and assert the actor (see
[Trusted-proxy actor header](#trusted-proxy-actor-header)), but kata models
identity, not per-actor permissions. Use it for trusted private deployments where
single-copy state and attribution are enough.
