KubeBolt docs
GitHub

API tokens

kbs_ service tokens and kbk_ API keys — how to create them, what scopes they get, and why a service token is rejected when it arrives from the internet.

REST API tokens authenticate non-interactive callers against /api/v1/* without a user session. Create and revoke them at Administration → API Tokens (/admin/api-tokens, Admin role).

They are not the same thing as the ingest tokens the agent and Falco use. Those start with kb_, live in a separate store, and are issued from Administration → Agents & Ingest → Agent Tokens. The two stores never cross: an ingest token can never authenticate a REST call, and a REST token can never authenticate the agent channel. That isolation is itself a security property.

Two kinds

kbs_ service tokenkbk_ API key
ForInternal machine callers that reach the API directly (backend-to-backend; on KubeBolt Cloud, Autopilot)Your own integrations, CI/CD and MCP hosts
Who may create itPlatform admin (in the open-source edition, any Admin)Admin
Default roleeditoreditor unless you pick viewer or admin
Default scopesThe service set (below)None — you pick
Through the bundled nginxRejectedWorks

Both are stored as SHA-256 hashes. The plaintext is returned exactly once, at creation, and is never recoverable — copy it then.

Both accept an optional expiry. Leave it empty for no expiry; revoking is immediate either way.

Creating one

In the UI, pick the kind, give it a label, and — for an API key — a role and its scopes. Or call the endpoint directly (type is service or apikey, and defaults to service when omitted):

curl -X POST https://your-kubebolt/api/v1/admin/api-tokens \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{
        "label": "ci-readonly",
        "type": "apikey",
        "role": "viewer",
        "scopes": ["/api/v1/resources", "/api/v1/insights"],
        "ttlHours": 720
      }'

The response carries the plaintext once, plus the token’s metadata. Every issue and revoke lands in the admin audit trail with the label, type, role and scopes — enough for a reviewer to judge whether the grant was appropriate.

Scopes

A scope is a URL path prefix. A token may call a path when any of its scopes is a prefix of it. * grants every authenticated route. An empty scope set denies everything — the check fails closed.

Scopes stack on top of the role, they do not replace it: a viewer token scoped to * still cannot write.

A service token created without explicit scopes gets the service set:

/api/v1/cluster/overview
/api/v1/insights
/api/v1/events
/api/v1/resources
/api/v1/mcp

The UI’s presets for API keys are cluster overview, resources, insights, events and metrics, plus two whole-surface options: MCP only (read-only tools), which collapses to /api/v1/mcp and nothing else, and Everything, which collapses to *.

MCP only is the one to reach for when a host only needs to read. It pins the token’s role to viewer on the server, whatever the creating request asks for, and defaults to 90 days of expiry. Before 2.2 the only way to reach the MCP server from the form was Everything, which handed the whole API to a key that needed read-only tools; if you set an MCP host up that way, replace its key.

No API token can reach /api/v1/admin/api-tokens, whatever its scopes say. Credentials are created, edited and revoked by a signed-in admin, so a leaked token cannot mint itself a broader one.

Cluster targeting

A REST token reads every cluster of its organization, or only the ones ticked for it. The list is set at creation and edited afterwards from the pencil on the token’s row (PATCH /api/v1/admin/api-tokens/{id}/clusters); an empty list means every cluster.

It is checked before any other shortcut, on every read path: the cluster a request names, the cluster a request that names none falls back to (pinned to one the token may read, so it cannot drift mid-conversation), everything scoped per cluster — findings, runtime events, insight history, bursts, mutes, the shift report, the fleet PromQL path — and the cluster lists themselves, so GET /api/v1/clusters and Kobi’s list_clusters name only what the token may read.

Within that list, a caller picks the cluster per request:

curl -H "Authorization: Bearer $TOKEN" \
     -H "X-KubeBolt-Cluster: prod-eu-west" \
     https://your-kubebolt/api/v1/cluster/overview

The value is the cluster’s context name as returned by GET /api/v1/clusters. Omit the header and you get the active cluster.

The public-edge restriction

A kbs_ service token presented over the public edge is rejected, even when it is otherwise valid.

The bundled nginx force-sets X-KubeBolt-Edge: public on everything it proxies — /api/, the WebSocket routes, and the port-forward proxy — using proxy_set_header, which overrides whatever the client sent. The API rejects service tokens carrying that header. A leaked kbs_ token is therefore useless from the internet.

Two things follow, and both are easy to trip over:

  1. It only holds if the API is unreachable except through the proxy. The control assumes the api Service is ClusterIP-only. Expose the API directly — a LoadBalancer, an Ingress that bypasses the bundled web container — and the header is never set, so the protection is gone. Keep it internal.
  2. Internal callers are unaffected. A caller reaching the API Service in-cluster (or the single-container image, which has no nginx) never carries the header.

What this means for MCP over HTTP

Kobi’s read-only MCP server is POST /api/v1/mcp, and it is API-token authenticated. Which token you need depends on where the client runs:

Send X-KubeBolt-Cluster alongside it to target a specific cluster.

Revoking

Delete the token in the UI, or DELETE /api/v1/admin/api-tokens/{id}. Revocation takes effect on the next request — there is no cached grant to wait out. The list shows each token’s prefix, label, role, scopes, and when it was last used, so an unused token is easy to spot and retire.