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 token | kbk_ API key | |
|---|---|---|
| For | Internal 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 it | Platform admin (in the open-source edition, any Admin) | Admin |
| Default role | editor | editor unless you pick viewer or admin |
| Default scopes | The service set (below) | None — you pick |
| Through the bundled nginx | Rejected | Works |
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:
- It only holds if the API is unreachable except through the proxy. The
control assumes the
apiService 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. - 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:
- Inside the network — a CI job in the same cluster, an agent calling the
API Service directly. A
kbs_service token works, and its default scopes already include/api/v1/mcp. - Through the UI URL — Claude Code on a laptop, a hosted agent, anything
that goes through the bundled nginx. A service token will be rejected at the
edge. Use a
kbk_API key created with MCP only, which is exactly this case:/api/v1/mcpas its whole surface andvieweras its role. Tick the clusters it may read while you are in the dialog. Read-only is enforced server-side too, from the tool catalogue, so a client cannot invoke a mutating tool even by name.
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.