API tokens
Create, use, and revoke the tokens that open the Kode API for a script, a CI job, or an integration.
Last updated
On this page
- Overview
- Reach
- Parts and verbs
- Presets
- Grants
- Only what the token created
- Limits of executions
- Who started a run
- Secrets
- Limits
- What a token cannot do
- How lists are narrowed
- Tasks
- Create an API token
- Create a token that runs one pipeline
- Create a token that runs capsules from one set of images
- Use a token with the CLI and the SDKs
- List your tokens
- Revoke a token
- Troubleshooting
Overview
An API token opens the /v1 API without a browser login. A CI job, a server that uses an SDK, and a third-party integration use it. You make a token, choose what it reaches and what it can do, and the platform shows the secret once.
A token starts with klt_. The platform stores only a digest of the secret, so a lost token must be revoked and replaced. Only you can list or revoke the tokens that you made.
A token acts as you, inside its limits. It can never do more than you can do. If your role in a team falls, the token falls with it.
Reach
The reach is the owner of the token. You set it when you make the token and cannot change it.
owner | The token reaches |
|---|---|
| Not set, or your handle | Your personal resources. |
| A team handle | The resources of that one team. You must be an active member. |
* | Your personal resources and the resources of every team that you belong to. The platform checks your membership at each request. |
A team that you join later is in the reach of a * token. A team that you leave or that pauses you is not.
Parts and verbs
A token holds verbs. There are nine, in three parts. No verb includes another: a token that can update a pipeline cannot start a run of it.
| Part | Verb | What it means |
|---|---|---|
| Monitoring | read | Get and list records. It includes quota and regions. |
| Monitoring | logs | Read the output of a capsule and the job logs of a run. read does not include it. |
| Resource management | create | Make a new workspace, environment, desk, or pipeline. |
| Resource management | update | Change a resource that exists: its packages, a new pipeline version, the environment of a desk, an intercept. |
| Resource management | lifecycle | Start, stop, restart, or terminate a resource, or wait for a capsule. |
| Resource management | snapshot | Take, clone, restore, or push a snapshot, or add a version to a volume. |
| Resource management | delete | Remove a resource. It is its own tick and is off in every preset. |
| Executions | run | Run code: create a capsule, start a pipeline run, or cancel a run. |
| Executions | exec | Attach to a running capsule. It is its own tick and is off in every preset. |
A kind admits only the verbs that mean something for it. A grant that names another verb is refused with 422.
kind | Verbs it admits |
|---|---|
workspaces, environments | read, create, update, lifecycle, snapshot, delete |
desks | read, create, update, delete |
capsules | read, logs, run, lifecycle, exec, delete |
pipelines | read, create, update, delete |
pipeline-runs | read, logs, run |
volumes | read, snapshot, delete |
snapshots | delete |
quota, regions | read |
The kind pipelines is the definitions and their versions. The kind pipeline-runs is the runs of a pipeline, their logs, and the cancel of a run. A selector on pipeline-runs matches the name of the pipeline of the run.
Presets
Set access to a list of parts. The platform expands each part into one grant on every kind in reach, with the verbs of that part that the kind admits. The expansion leaves out delete and exec.
access value | Verbs it gives |
|---|---|
monitoring | read, logs |
management | create, update, lifecycle, snapshot |
executions | run |
destroy | delete, on every kind that admits it |
exec | exec, on every kind that admits it |
Use any subset, for example ["monitoring", "management"]. In the console, three switches set the parts: Monitoring, Resource management, and Executions. Two ticks, Include delete and Include exec, add destroy and exec.
Send access or grants, never both. The platform stores the expanded grants. Use grants for control of one kind or one resource.
Grants
A grant has a kind, a list of verbs, and a list of resources. It can also have own and, on capsules, limits. A request passes when one grant covers its kind, its verb, and the resource that it names.
| Part | Values |
|---|---|
kind | workspaces, environments, desks, capsules, pipelines, pipeline-runs, volumes, snapshots. quota and regions are read-only and take no selector. |
verbs | The verbs that the kind admits. |
resources | * for all, an exact id or name, or a prefix that ends in *, such as vendor-*. |
own | true to cover only what this token created. |
limits | The limits of a capsule create. |
A selector matches the id or the name of the resource. The prefix is literal: vendor-* matches vendor-a and not vendors-a. A * inside a selector is not a wildcard.
A route that names a second resource needs a grant for it. An example is a desk that connects to an environment. A missing grant on one of them is a 401.
Only what the token created
With own: true, a grant covers an object only when this token created it. The platform stamps the label k-ode.dev/created-by-token on each workspace, environment, desk, capsule, pipeline, and run that a token makes. Two integrations in one team cannot see or stop the capsules and runs of each other.
A create with no name is permitted when the grant has own, because the token can then touch only what it made. own is refused on volumes and snapshots, which a token never creates.
Limits of executions
A capsules grant that holds run or exec can carry limits. A create that is outside the limits is refused with 401.
| Limit | Meaning | When not set |
|---|---|---|
images | Image selectors that the image of the create must match. * permits any image. | Any image. When limits is set without images, only the platform default image. |
max_timeout_s | The longest timeout_secs of a create, up to 86400. | 3600 |
network | false refuses a create that asks for network. | true |
The cap of runs in flight, max_live_runs, counts the live capsules and pipeline runs that one token started. The range is 1 to 50 and the default is 5. The next start gets 429 with this token has {n} runs in flight; the limit is {max}. Another run can start when one ends. The cap is a limit on cost for a token that leaks. It is not a rate limit.
Who started a run
A pipeline run or a capsule that a token starts records the token. The record holds apiToken with the name and the first eight characters of the id. The console shows API token {name} and never the handle of the creator.
A manual start of a pipeline takes an optional key. A second start with the same key answers the run that the first start made, so a retry cannot start a duplicate. Each token and each person has its own keys.
Secrets
A token cannot send a request that names a secret. A request is refused with 401 when a string in its body holds ${{ secrets., at any depth, or when a pipeline definition has secrets. A token cannot read a secret value.
A token can start a pipeline that uses a secret, if someone else wrote the pipeline. The pipeline uses the secret. The holder of the token never receives it.
Limits
| Limit | Value |
|---|---|
| Name | 1 to 60 characters |
| Expiry | 1 to 365 days; 90 when not set |
| Grants in one token | 20 |
| Selectors in one grant | 50 |
| Runs in flight | 1 to 50; 5 when not set |
| Live tokens for one creator and one owner | 25 |
A token always expires. The list shows the expiry and the time of the last use. The platform records the last use at most once an hour.
What a token cannot do
A token cannot reach the routes for tokens, keys, teams, invitations, secrets, connectors, requests, sign-in, or the console administration. It cannot read a secret value. It cannot open a tool server or an SSH session. exec reaches the attach route of a capsule and no other route. These routes answer 401 missing or invalid token, the same answer as a wrong token.
A request for a resource outside the grants is the same 401. A request with ?owner= or team for a team that a one-team token does not serve is 403 with this token acts only for and the team.
How lists are narrowed
A list route answers only the items that the grants select. A grant with * and no own returns the whole page. A grant with selectors or own removes the other items from the page, so a page can be short or empty while more pages follow. Follow the x-next-cursor header until it is absent.
Tasks
Create an API token
Reason for the task
Give a program read access, and the right to manage resources, in one owner.
Job set-up
- Access: you, signed in with the console or a CLI login. A token cannot make a token. A team token needs an active team member.
- References:
POST /v1/tokens.
The secret opens the API within the reach and the parts. Keep it out of repositories and logs.
Procedure
Create the token with
name,owner, andaccess. In the console, select New token in Profile settings.t = kl.request("POST", "/v1/tokens", body={ "name": "ci", "owner": "acme", "access": ["monitoring", "management"]})const t = (await kl.request("POST", "/v1/tokens", { name: "ci", owner: "acme", access: ["monitoring", "management"]})) as { _id: string; token: string };var t map[string]anyerr := kl.Request("POST", "/v1/tokens", kloudlite.RequestOptions{ Body: map[string]any{ "name": "ci", "owner": "acme", "access": []string{"monitoring", "management"}, }, Out: &t,})if err != nil { log.Fatal(err)}t = kl.request("POST", "/v1/tokens", body: { name: "ci", owner: "acme", access: ["monitoring", "management"]})String t = kl.request(new Kloudlite.RawRequest().method("POST").path("/v1/tokens") .body("{\"name\":\"ci\",\"owner\":\"acme\",\"access\":[\"monitoring\",\"management\"]}"));kode request POST /v1/tokens --body '{"name": "ci", "owner": "acme", "access": ["monitoring", "management"]}'Put the
tokenvalue in your secret store. The platform does not show it again.
Close-out
- Show the tokens as in task 10-20-00-005. Make sure that the new token is listed.
Create a token that runs one pipeline
Reason for the task
Give a vendor the right to run the pipeline nightly-report and to read its runs and logs, and nothing else.
Job set-up
- Access: you. The
owneris a team where you are an active member. - References:
POST /v1/tokens.
The vendor can start nightly-report and read its logs. A log can hold data that the pipeline prints. Check what the pipeline prints.
Procedure
Create the token with one grant on
pipeline-runsforread,logs, andrun. Set two runs in flight and 30 days.t = kl.request("POST", "/v1/tokens", body={ "name": "vendor-report", "owner": "acme", "expires_in_days": 30, "max_live_runs": 2, "grants": [ { "kind": "pipeline-runs", "verbs": ["read", "logs", "run"], "resources": ["nightly-report"] } ]})const t = (await kl.request("POST", "/v1/tokens", { name: "vendor-report", owner: "acme", expires_in_days: 30, max_live_runs: 2, grants: [ { kind: "pipeline-runs", verbs: ["read", "logs", "run"], resources: ["nightly-report"] } ]})) as { _id: string; token: string };var t map[string]anyerr := kl.Request("POST", "/v1/tokens", kloudlite.RequestOptions{ Body: map[string]any{ "name": "vendor-report", "owner": "acme", "expires_in_days": 30, "max_live_runs": 2, "grants": []map[string]any{ { "kind": "pipeline-runs", "verbs": []string{"read", "logs", "run"}, "resources": []string{"nightly-report"}, }, }, }, Out: &t,})if err != nil { log.Fatal(err)}t = kl.request("POST", "/v1/tokens", body: { name: "vendor-report", owner: "acme", expires_in_days: 30, max_live_runs: 2, grants: [ { kind: "pipeline-runs", verbs: ["read", "logs", "run"], resources: ["nightly-report"] } ]})String t = kl.request(new Kloudlite.RawRequest().method("POST").path("/v1/tokens") .body("{\"name\":\"vendor-report\",\"owner\":\"acme\",\"expires_in_days\":30,\"max_live_runs\":2,\"grants\":[{\"kind\":\"pipeline-runs\",\"verbs\":[\"read\",\"logs\",\"run\"],\"resources\":[\"nightly-report\"]}]}"));kode request POST /v1/tokens --body '{"name": "vendor-report", "owner": "acme", "expires_in_days": 30, "max_live_runs": 2, "grants": [{"kind": "pipeline-runs", "verbs": ["read", "logs", "run"], "resources": ["nightly-report"]}]}'Give the
tokenvalue to the vendor through your secret store.Give the vendor the rule to send a
keywith each start, such as the date. A retry with the same key answers the first run.
Close-out
- Show the tokens. Make sure that the grants of the new token name only
pipeline-runs.
Create a token that runs capsules from one set of images
Reason for the task
Give a CI job the right to run capsules from node: images, for at most 600 seconds, and to see only its own capsules.
Job set-up
- Access: you. The
owneris your handle or a team where you are an active member. - References:
POST /v1/tokens.
The job can run any code in a capsule that it starts. Choose the image selector with care.
Procedure
Create the token with one grant on
capsulesthat hasownandlimits. Set ten runs in flight.t = kl.request("POST", "/v1/tokens", body={ "name": "ci-node", "owner": "acme", "max_live_runs": 10, "grants": [ { "kind": "capsules", "verbs": ["read", "logs", "run", "lifecycle"], "resources": ["*"], "own": True, "limits": { "images": ["node:*"], "max_timeout_s": 600, "network": True } } ]})const t = (await kl.request("POST", "/v1/tokens", { name: "ci-node", owner: "acme", max_live_runs: 10, grants: [ { kind: "capsules", verbs: ["read", "logs", "run", "lifecycle"], resources: ["*"], own: true, limits: { images: ["node:*"], max_timeout_s: 600, network: true } } ]})) as { _id: string; token: string };var t map[string]anyerr := kl.Request("POST", "/v1/tokens", kloudlite.RequestOptions{ Body: map[string]any{ "name": "ci-node", "owner": "acme", "max_live_runs": 10, "grants": []map[string]any{ { "kind": "capsules", "verbs": []string{"read", "logs", "run", "lifecycle"}, "resources": []string{"*"}, "own": true, "limits": map[string]any{ "images": []string{"node:*"}, "max_timeout_s": 600, "network": true, }, }, }, }, Out: &t,})if err != nil { log.Fatal(err)}t = kl.request("POST", "/v1/tokens", body: { name: "ci-node", owner: "acme", max_live_runs: 10, grants: [ { kind: "capsules", verbs: ["read", "logs", "run", "lifecycle"], resources: ["*"], own: true, limits: { images: ["node:*"], max_timeout_s: 600, network: true } } ]})String t = kl.request(new Kloudlite.RawRequest().method("POST").path("/v1/tokens") .body("{\"name\":\"ci-node\",\"owner\":\"acme\",\"max_live_runs\":10,\"grants\":[{\"kind\":\"capsules\",\"verbs\":[\"read\",\"logs\",\"run\",\"lifecycle\"],\"resources\":[\"*\"],\"own\":true,\"limits\":{\"images\":[\"node:*\"],\"max_timeout_s\":600,\"network\":true}}]}"));kode request POST /v1/tokens --body '{"name": "ci-node", "owner": "acme", "max_live_runs": 10, "grants": [{"kind": "capsules", "verbs": ["read", "logs", "run", "lifecycle"], "resources": ["*"], "own": true, "limits": {"images": ["node:*"], "max_timeout_s": 600, "network": true}}]}'Put the
tokenvalue in the secret store of the CI system.
Close-out
- Show the tokens. Make sure that the grant shows
ownand thelimits.
Use a token with the CLI and the SDKs
Reason for the task
Run a script, a CI job, or an SDK client with a token and no browser login.
Job set-up
- Access: the secret of the token.
- References: SDKs.
Procedure
Set the API origin and the token in the environment of the program. The SDKs and
koderead both.export KODE_URL=https://dev.kloudlite.ioexport KODE_TOKEN=klt_...Run the program. For a direct call, send the token as a bearer.
curl -H "Authorization: Bearer $KODE_TOKEN" https://dev.kloudlite.io/v1/workspaces
Close-out
- Make sure that the call answers
200. A401means that the token is wrong, expired, revoked, or outside its grants for that route.
List your tokens
Reason for the task
Find the tokens that you made, their reach, their expiry, and their last use.
Job set-up
- Access: you, signed in with the console or a CLI login.
- References:
GET /v1/tokens.
Procedure
Show the tokens. Add
?owner=to see the tokens of one reach.for t in kl.request("GET", "/v1/tokens"): print(t["_id"], t["name"], t["reach"], t["expiresAt"], t["lastUsedAt"])const tokens = (await kl.request("GET", "/v1/tokens")) as { _id: string; name: string; expiresAt: string; lastUsedAt: string | null }[];for (const t of tokens) console.log(t._id, t.name, t.expiresAt, t.lastUsedAt);var tokens []map[string]anyerr := kl.Request("GET", "/v1/tokens", kloudlite.RequestOptions{Out: &tokens})if err != nil { log.Fatal(err)}for _, t := range tokens { fmt.Println(t["_id"], t["name"], t["expiresAt"], t["lastUsedAt"])}kl.request("GET", "/v1/tokens").each { |t| puts [t["_id"], t["name"], t["expiresAt"], t["lastUsedAt"]].join(" ") }System.out.println(kl.request(new Kloudlite.RawRequest().method("GET").path("/v1/tokens")));kode request GET /v1/tokens
Close-out
- Make sure that each token in the list is still in use. A
lastUsedAtofnullmeans that no use is recorded.
Revoke a token
Reason for the task
Stop a token that is unused or exposed.
Job set-up
- Access: you. Only the creator can revoke a token.
- References:
DELETE /v1/tokens/{id}.
A revoked token stops at once. Programs that use it fail.
Procedure
Find the
_idof the token as in task 10-20-00-005.Delete the token. Replace
{id}with the_idof the token.kl.request("DELETE", "/v1/tokens/{id}")await kl.request("DELETE", "/v1/tokens/{id}");if err := kl.Request("DELETE", "/v1/tokens/{id}", kloudlite.RequestOptions{}); err != nil { log.Fatal(err)}kl.request("DELETE", "/v1/tokens/{id}")kl.request("DELETE", "/v1/tokens/{id}");kode request DELETE /v1/tokens/{id}
Close-out
- Show the tokens. Make sure that the
_idis gone.
Troubleshooting
| Symptom | Possible cause | Corrective action |
|---|---|---|
401 with missing or invalid token | The token is wrong, expired, or revoked, or the route, verb, or resource is outside its grants. | Check the expiry and the grants in the list. Create a new token if needed. |
401 on POST /v1/capsules | The token has no run on capsules, or the create is outside its limits. | Add run to the grant. Use an image, a timeout, and a network setting that the limits permit. |
401 on a request with an env value of ${{ secrets.NAME }} | A token cannot send a request that names a secret. | Set the value in the pipeline definition, and start the pipeline with the token. |
401 on a pipeline save | The definition has secrets, or the token has no create or update on pipelines. | Remove secrets from the definition, or use a person to save it. |
401 on a start of a run | The token has update on pipelines but no run on pipeline-runs. | Add run on pipeline-runs. No verb includes another. |
429 with runs in flight | The token has its limit of live capsules and runs. | Wait for a run to end, or raise max_live_runs in a new token. |
403 with this token acts only for and a team | The request names a team other than the reach of the token. | Use the team of the token, or a token with the reach *. |
400 with give the token a name | The name is empty. | Set name. |
404 with no such owner | The owner is not you or a team where you are an active member. | Use your handle or one of your teams. |
409 with pick a username before making a token | The account has no handle. | Choose a handle in the console. |
409 with tokens: and in use | You have 25 live tokens for that owner. | Revoke one, then create the new token. |
422 with is not a verb of | The grant names a verb that its kind does not admit. | Use a verb from the table of kinds. |
422 with grants[0].resources | A selector is not a name, an id, or a prefix that ends in *. | Use letters, digits, -, _, ., /, and one final *. |
422 with max_live_runs | The value is out of range, or the token has no run or exec verb. | Use 1 to 50, and add a grant with run or exec. |
| A list is shorter than expected | The grants select only some items, or own hides the rest. | Follow x-next-cursor, or widen the selectors. |
| The secret is lost | The platform shows it once. | Revoke the token and create a new one. |