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 safe inspection endpoints
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
In auto mode, loopback-only bindings can run without tokens, but non-loopback exposure requires an admin token. Treat none as a local-development mode only. 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
  • scoped MCP OAuth resolution, where enabled by a runtime path, uses short-lived leases scoped to one server, slot, and approved scope hash
  • 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
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
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. 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. Current OAuth-backed MCP config is account plumbing and fail-closed validation only; it does not expose OAuth-backed MCP tools yet. 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; OAuth-backed MCP tool/resource use is not exposed yet.
  • Vendor manual secret-store check: GitHub explicit Docker MCP config was validated outside the local harness with evidence under /tmp/kheish-github-mcp-run.7lFCOV: 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. Only enable it on isolated daemon instances and reduce the level again once the investigation is complete.