Skip to main content

Security and auth

Kheish is an execution system. Security decisions affect both the control plane and the runtime itself.

Control-plane authentication

The daemon includes built-in bearer authentication for the HTTP control plane. It supports:
  • an admin token for mutating operations
  • an optional read-only token for ordinary inspection endpoints
Read-only tokens are method-scoped for ordinary GET, HEAD, and OPTIONS routes, but sensitive read paths such as raw asset bytes, run debug artifacts, daemon secret inventory, account-backed auth inventory, and brokered runtime-auth subject or lease inspection require an admin token. Read-only access can still reveal session content, run views, runtime metadata, and connector summaries, so do not issue it to untrusted tenants or browser clients you do not control. At startup, auth mode is controlled through:
  • --http-auth-mode auto
  • --http-auth-mode bearer
  • --http-auth-mode none
The related token inputs are:
  • --http-admin-token or --http-admin-token-file
  • --http-readonly-token or --http-readonly-token-file
  • KHEISH_DAEMON_ADMIN_TOKEN
  • KHEISH_DAEMON_READONLY_TOKEN
The CLI client uses KHEISH_DAEMON_TOKEN or KHEISH_DAEMON_TOKEN_FILE when talking to a bearer-protected daemon. Docker examples set both the server token file and client token file variables so commands inside the container can authenticate without repeating --token. In auto mode, loopback-only bindings can run without tokens, but non-loopback exposure requires an admin token. The daemon builder enforces the same boundary as the CLI, so embedding Kheish cannot accidentally bind an unauthenticated control plane outside loopback. Treat none as a local-development mode only. Admin and read-only tokens must be distinct. The daemon rejects duplicate inline tokens at startup, rejects identical token-file paths, and refuses all bearer tokens if hot rotation temporarily makes the effective admin and read-only file contents identical. Fix the files to distinct values and the next request reloads them.

Control-plane auth audit and token rotation

When the daemon serves, auth and CORS failures are written to:
<state_root>/control-plane-auth/audit.jsonl
Each line is JSON and includes the event, timestamp, method, path, reason, allowed-origin input when present, required access where relevant, and the remote socket address when the HTTP server has it. Token bytes are never written. The audit file is created with owner-only permissions on Unix. Current events are:
  • auth_failure
  • auth_rate_limited
  • cors_origin_rejected
  • cors_origin_rate_limited
The daemon serializes audit writes and rate-limits failure audit volume per reason/window. A flood therefore returns 429 for repeated auth failures and writes one transition event instead of unbounded duplicate lines. Token-file inputs are re-read when their file metadata changes:
./target/debug/kheish-daemon serve \
  --http-auth-mode bearer \
  --http-admin-token-file /run/secrets/kheish-admin-token \
  --http-readonly-token-file /run/secrets/kheish-readonly-token
After rotation, old tokens are invalidated by the next request that observes the file change. Keep admin/read-only files distinct during rotation; if a deployment briefly writes the same token to both files, the daemon fails closed until the files diverge again. For the full production token and route-secret rotation runbook, including validation commands and backup/restore expectations, read Production runbooks.

Control-plane CORS

The daemon’s browser CORS policy is separate from authentication. By default, the control plane accepts browser origins whose host is loopback-compatible, such as http://localhost:5173, http://127.0.0.1:5173, or http://[::1]:5173. This keeps local web development ergonomic without opening the daemon to remote origins. For a production local gateway or a hosted UI that proxies through a fixed loopback daemon endpoint, set an exact browser allowlist:
./target/debug/kheish-daemon serve \
  --http-cors-allow-origin http://localhost:5173 \
  --http-cors-allow-origin http://127.0.0.1:3000
The KHEISH_HTTP_CORS_ALLOW_ORIGINS environment variable accepts the same comma-delimited origin list. Each origin must be an exact http or https loopback origin without a path, query, fragment, or wildcard. CORS is not an auth substitute; keep bearer auth enabled for every non-loopback bind. Connector ingress routes do not reuse these control-plane bearer tokens. Each connector keeps its own authentication model, such as a per-connector HTTP bearer token, a Slack signing secret, or a Telegram secret token. Daemon-managed connectors still use the normal control-plane auth boundary for their CRUD endpoints. The transport auth they carry is separate from the operator auth used to mutate them.

Runtime safety controls

The main runtime safety controls are:
  • permission modes and rule scopes
  • approval-gated tool execution
  • hook-driven policy enforcement
  • workspace-root and path validation
  • connector-specific authentication such as Slack signing secrets or Telegram secret tokens

Brokered runtime auth

AuthManager remains the root of trust for long-lived deployment credentials such as route secrets, connector secrets, and imported account-backed auth. At execution time, the daemon does not hand those root credentials directly to agents, sidecars, or delegated work. Instead, it derives three brokered objects:
  • AuthSubject: the execution identity that is asking for credential-backed work
  • CredentialGrant: the allowed audience for that subject after scope checks
  • CredentialLease: the short-lived opaque lease actually used for one route resolution or one connector sidecar credential request
In practice this means:
  • provider routes are resolved through short-lived route leases
  • child-process external connectors receive short-lived connector leases scoped to explicit env keys and concrete backing secret refs
  • scoped MCP OAuth resolution uses short-lived leases scoped to one server, slot, and approved scope hash, and HTTP MCP clients re-authorize before each call
  • revocation is immediate for future resolutions and active leases
  • audit can refer to subjects, principals, grants, and leases without exposing the root secret itself
Common subject ids are session- or agent-derived, such as session:demo or agent:agent-7. Those ids are the same identities you will see reflected in audit metadata and runtime auth inspection.

Credential scopes

Kheish separates visibility from credential usage.
  • CapabilityScope controls what a session or child can see and call
  • CredentialScope controls which auth-backed resources that execution may actually resolve
CredentialScope currently supports:
  • route_allow
  • route_deny
  • connector_allow
  • connector_deny
  • connector_credential_allow
  • connector_credential_deny
  • mcp_server_allow
  • mcp_server_deny
Example:
{
  "route_allow": ["openai", "anthropic"],
  "connector_allow": ["slack-prod"],
  "connector_credential_allow": ["slack-prod:BOT_TOKEN"],
  "mcp_server_deny": ["github"]
}
Important rule:
  • if you scope connectors with connector_allow or connector_deny and omit connector_credential_allow, concrete connector secret env keys default to denied
Sessions can persist a credential scope directly, and sidechains can request a narrower child credential scope. Child scopes are always intersected with the parent and cannot widen it.

Inspect and revoke brokered auth

Operator-facing auth inspection is exposed on both the HTTP API and the CLI. HTTP:
  • GET /v1/runtime/auth/subjects/{subject_id}
  • POST /v1/runtime/auth/subjects/{subject_id}/revoke
  • GET /v1/runtime/auth/leases/{lease_id}
  • POST /v1/runtime/auth/leases/{lease_id}/revoke
  • POST /v1/runtime/auth/slots/{slot_id}/revoke
CLI:
./target/debug/kheish-daemon runtime auth subject session:demo
./target/debug/kheish-daemon runtime auth revoke-subject session:demo
./target/debug/kheish-daemon runtime auth lease route-lease-abc123
./target/debug/kheish-daemon runtime auth revoke-lease route-lease-abc123
./target/debug/kheish-daemon runtime auth revoke-slot openai.prod
subject status shows whether a subject has been revoked plus the currently active route, connector, and MCP lease ids. lease status shows the stored lease payload, whether it has been revoked, and whether it is still active; the operator view does not expose the broker’s internal token digest. revoke-slot revokes active route, connector, and MCP leases tied to one auth slot without deleting the slot itself. Account-backed auth status has a separate redacted surface:
./target/debug/kheish-daemon runtime auth accounts list
./target/debug/kheish-daemon runtime auth accounts get openai.prod
./target/debug/kheish-daemon runtime auth accounts refresh openai.prod
./target/debug/kheish-daemon runtime auth accounts revoke openai.prod
Use this for OpenAI Codex account auth, Anthropic Claude account auth, and MCP OAuth accounts. Reads return status metadata only; no endpoint returns token bytes.

Daemon-managed route secrets

The recommended operator path is:
  1. define named routes with auth_ref
  2. populate those refs through kheish-daemon secrets ...
  3. start the daemon with --routes-file ...
auth_ref is the stable daemon-owned identifier for one secret slot. Routes resolve credentials through that identifier instead of inlining API keys in the route file. Example route file:
version = 1
default_route = "openrouter"

[routes.openrouter]
driver = "openrouter"
default_model = "openai/gpt-5.4-mini"
model_support = "any"
auth_ref = "openrouter.prod"

[routes.openai]
driver = "openai"
default_model = "gpt-5.4"
auth_ref = "openai.prod"

[routes.anthropic]
driver = "anthropic"
default_model = "claude-opus-4-6"
auth_ref = "anthropic.prod"
Bootstrap the referenced slots before daemon startup:
export KHEISH_AUTH_STORE_MASTER_KEY="$(./target/debug/kheish-daemon secrets generate)"

./target/debug/kheish-daemon secrets set openrouter.prod \
  --offline \
  --state-root .kheish-daemon-data \
  --provider openrouter \
  --from-env OPENROUTER_API_KEY

./target/debug/kheish-daemon secrets set openai.prod \
  --offline \
  --state-root .kheish-daemon-data \
  --provider openai \
  --from-env OPENAI_API_KEY

./target/debug/kheish-daemon secrets set anthropic.prod \
  --offline \
  --state-root .kheish-daemon-data \
  --provider anthropic \
  --from-env ANTHROPIC_API_KEY
Generate this key once per persistent state_root and keep reusing it. Replacing it later makes existing encrypted secret slots unreadable. When account-backed auth is preferable, import it into the same secret slots:
./target/debug/kheish-daemon secrets import-codex openai.prod \
  --offline \
  --state-root .kheish-daemon-data

./target/debug/kheish-daemon secrets import-claude-code anthropic.prod \
  --offline \
  --state-root .kheish-daemon-data
OpenRouter routes are API-key-only at the daemon auth layer. They do not use import-codex or OpenAI account-backed auth material. Operational rules:
  • the daemon refuses to start when a configured auth_ref is missing
  • one auth_ref can be reused by multiple routes without copying secret material into each route definition
  • runtime get and the secrets endpoints expose auth_ref metadata, never raw secret values
  • secrets set on an existing ref is the standard rotation path for static API-key slots
  • actual route use still goes through the broker, so route selection can succeed while auth resolution is later denied by the execution’s effective CredentialScope

Secret storage

Kheish stores daemon-managed route secrets under state_root/auth/global-slots.json. New writes use the encrypted auth-store envelope when KHEISH_AUTH_STORE_MASTER_KEY or KHEISH_AUTH_STORE_MASTER_KEY_FILE is configured. The loader still accepts the older plaintext JSON shape for backward compatibility, so do not assume an existing state root is already encrypted until it has been rewritten through the current store path. kheish-daemon secrets list and kheish-daemon secrets get <secret_ref> return slot metadata only:
  • slot_id
  • provider
  • mode
  • summary
  • updated_at_ms
This keeps the operator view inspectable without making the daemon a secret reveal service. The same secret store also backs daemon-managed connector and MCP secrets. Runtime connector and secret APIs can accept write-only secret values and bind them to secret-store references, but subsequent reads only expose redacted metadata such as:
  • whether the secret is configured
  • whether the saved connector points at inline, env, or secret_ref
  • which secret_ref or env name is bound
To persist connector or MCP secret writes through HTTP or CLI runtime APIs, the daemon itself must be started with KHEISH_AUTH_STORE_MASTER_KEY or KHEISH_AUTH_STORE_MASTER_KEY_FILE. Without one of those, the daemon can still read env-backed config, but it cannot store new runtime-managed secret-store entries. Built-in MCP catalog credentials use generic opaque slots under mcp.<entry-id>.<credential-env>, for example mcp.linear.LINEAR_API_KEY. Use kheish-daemon mcp auth slots <entry-id> to inspect the slot names and kheish-daemon mcp auth set <entry-id> ... --offline --state-root <state-root> to store or rotate them before startup. Start the daemon with the same state root and auth-store master key. Explicit MCP config can reference custom slots with bearer_token_secret_ref, http_header_secret_refs, and env_secret_refs; stdio MCP servers using env_secret_refs run with a restricted child environment. GitHub’s explicit Docker MCP path uses this same secret-store pattern with mcp.github.GITHUB_PERSONAL_ACCESS_TOKEN; the token is injected into the child process from the daemon store rather than kept in the daemon runtime environment. Restart the daemon after rotating a secret used by an already loaded MCP server. Spec-compliant HTTP MCP OAuth accounts use OAuth slots under mcp.oauth.<id> and are created with kheish-daemon mcp oauth login. The login flow writes through the daemon account API into the same encrypted auth store, then mcp oauth status, mcp oauth refresh, and runtime auth accounts ... expose only redacted account metadata. OAuth-backed MCP config must reference the slot with oauth_slot_ref, oauth_resource, and oauth_scopes; Kheish rejects scope escalation on refresh and does not let agents consent to broader scopes. OAuth-backed HTTP MCP servers initialize only inside a scoped runtime path, and the client re-resolves brokered OAuth material before each call so slot deletion, subject revocation, and token rotation fail closed instead of reusing stale headers. For container platforms, the same store key can be supplied through KHEISH_AUTH_STORE_MASTER_KEY_FILE. That is the recommended path when your orchestrator already mounts secret files. The same pattern also exists for control-plane bearer tokens:
  • KHEISH_DAEMON_ADMIN_TOKEN_FILE
  • KHEISH_DAEMON_READONLY_TOKEN_FILE
Alongside the long-lived auth slots, the daemon also persists broker revocation and issued-lease state under the same state-root auth area. Back up that broker state together with the auth store when you need durable revocation history across restarts.

Evidence note

  • Code verified: crates/kheish-auth/src/store.rs, crates/kheish-auth/src/manager.rs, crates/kheish-auth/src/broker.rs, crates/kheish-auth/src/backends/openai.rs, crates/kheish-auth/src/backends/anthropic.rs, crates/kheish-auth/src/backends/mcp_oauth.rs, crates/kheish-daemon/src/state.rs, crates/kheish-daemon/src/api/handlers.rs.
  • CLI verified: secrets set, secrets import-codex, secrets import-claude-code, runtime auth subject, runtime auth lease, runtime auth accounts, mcp auth, and mcp oauth surfaces exist.
  • Daemon live tested: yes for generic MCP OAuth account login/refresh/logout with scripts/e2e/mcp_oauth_protocol_true_binary.sh; bearer MCP secret-store behavior is covered by scripts/e2e/mcp_secret_store_true_binary.sh; scoped OAuth-backed MCP calls are additionally unit-validated at the client broker boundary.
  • Vendor manual secret-store check: GitHub explicit Docker MCP config was validated outside the local harness with a temporary evidence directory: the token stored as mcp.github.GITHUB_PERSONAL_ACCESS_TOKEN in Kheish, true daemon startup, OpenAI gpt-5.4, runtime source: "codex_config", and a real mcp__github__get_me tool call.
  • Provider-specific tested: deterministic account-backend tests exist for OpenAI and Anthropic; validate live Codex/Claude account refresh on your production credential source before relying on it.

Signed external-action audit

Kheish keeps a separate append-only audit ledger for external actions such as provider requests, connector delivery, hooks, and other networked or system-facing boundaries. Each record is signed and chained. The stored fields include:
  • action_id, phase, and kind
  • session_id, agent_id, run_id, and tool_call_id
  • principal_id, parent_principal_id, and grant_id when known
  • target
  • request_digest and response_digest
  • prev_hash, record_hash, signature_alg, key_id, and signature
Use these surfaces to inspect one run’s external audit:
  • GET /v1/runs/{run_id}/external-actions
  • ./target/debug/kheish-daemon runs external-actions <run_id>
Key material:
  • by default the daemon persists its Ed25519 audit signing key at state_root/audit-signing.key
  • you can override that with KHEISH_EXTERNAL_ACTION_AUDIT_SIGNING_KEY or KHEISH_EXTERNAL_ACTION_AUDIT_SIGNING_KEY_FILE
The audit is fail-closed. If the signed external-action audit cannot initialize or becomes unavailable, future external actions are rejected instead of proceeding without durable signed records.

Other deployment secrets

Treat API keys, connector tokens, signing secrets, MCP credentials, and control-plane bearer tokens as deployment secrets. Keep them out of page content, logs, and exported debug artifacts whenever possible.

Exposure guidance

Do not expose the control plane beyond localhost without authentication, and do not enable broad delivery or connector ingress without reviewing the downstream trust boundary.

Debug capture caution

Full debug capture can include sensitive prompt and provider payload data. Redacted debug capture summarizes audio transcription attachment blocks, but it is still operational evidence, not a privacy boundary. Only enable full capture on isolated instances and reduce the level again once the investigation is complete. When debug evidence must be retained in production-like environments, start the daemon with KHEISH_DEBUG_CAPTURE_KEY or KHEISH_DEBUG_CAPTURE_KEY_FILE so persisted debug artifact bodies are encrypted at rest. Use KHEISH_DEBUG_REDACT_TOKENS or KHEISH_DEBUG_REDACT_TOKENS_FILE for deployment-specific tokens that should be scrubbed from debug bundles before persistence.