Documentation

Credentials and tokens

Every secret in the system - what it looks like, what it can do, how long it lives, and how to revoke it.

Most confusion about Kino is really confusion about which string goes where. They all look alike — long random text — but they do very different jobs.

The rule underneath everything: control mints, relay checks, agent and manager carry.

The whole set, at a glance#

Credential Looks like Held by Lifetime Revoke by
Account key kck_… Kino SSH Manager Until revoked Revoking it on Account keys
Enroll key kce_… the install command One use Deleting the machine
Agent key kca_… the target machine Until the machine is deleted Deleting the machine
Relay token JWT agent / manager, in memory 24 h / 1 h Waiting for expiry
Web session cookie or JWT your browser / API client 7 days Signing out
Relay enroll code short token the relay operator One use —
Static relay token your own string relay, agent, manager Forever Changing it and restarting

Every credential Kino Cloud issues is stored only as a SHA-256 hash and displayed exactly once. There is no “show me that key again”.

Account keys (kck_)#

The credential you paste into Kino SSH Manager. It stands in for signing in: it identifies your account to the API without the app ever holding your password.

Can: list, create and delete your machines; call connect to get a manager token and the agent’s location.

Cannot: create more account keys. That is session-only on purpose — if a leaked key could mint its own successors, revoking it would not actually stop anything.

Only the hash is stored, and it is checked against the database on every single use, so revocation takes effect on the next request. last_used_at is recorded, which is how you tell a forgotten key from a live one before you revoke it.

Create and revoke them on Account keys.

Enroll keys (kce_)#

One per machine, generated when you add it, and embedded in the install URL:

curl -fsSL https://kino.dpdns.org/install/kce_… | sudo sh

The key is the credential — that URL is public, which is why it must be treated as a secret.

Fetching the URL does not consume it; the script is just text until you run it. Running it POSTs to /api/agents/exchange, which is one-time and transactional: the first execution wins and every later attempt gets 401 invalid or already-used enroll key.

If an install fails partway and you need to retry, delete the machine and add it again to get a fresh key.

Agent keys (kca_)#

The machine’s long-lived credential, handed over once in exchange for the enroll key and written to /etc/kino-agent/kino-agent.env (mode 0600, root-owned).

Its only power is calling /api/agents/refresh to trade itself for a short-lived relay token. It cannot list your machines, cannot read anything about your account, and cannot reach any other agent — the token it receives is scoped to its own agent_id.

This is the design’s churn cutoff: an agent key is not access, it is permission to keep asking for access. Stop answering, and access ends within a day.

Relay tokens (JWTs)#

The only credential the relay ever sees. Signed with the controller’s Ed25519 key, verified by the relay with the public half — so a relay can check a token but can never forge one. That asymmetry is what makes community-run relays possible.

Claims:

{ "sub": "agent", "agent_id": "6b1d…", "exp": 1785000000 }
Role Issued to Default life Scope
agent a machine, via /api/agents/refresh 24 hours Its own agent_id
manager the desktop app, via /api/machines/{id}/connect 1 hour The one machine you are dialling

A manager token for box-1 cannot reach box-2, and cannot register an agent at all. The agent refreshes in the background at roughly ¾ of each token’s life, so a token never expires mid-session.

There is no revocation list. Short lifetimes are the revocation mechanism — which is why the numbers are as small as they are.

The long-lived variant

POST /api/tokens mints relay tokens with a lifetime you choose (default 30 days, maximum 365) and an optional * wildcard scope. It exists for self-hosted setups that want to configure an agent once and forget it. Prefer the short-lived refresh flow wherever you can — a 365-day wildcard token is a password with no revocation.

Web sessions#

Two flavours, same account:

  • The browser gets an ordinary session cookie, with CSRF protection on every form.
  • API clients get an HS256 JWT from /api/auth/signin, good for 7 days, passed as Authorization: Bearer.

Session tokens are signed with a different key from relay tokens, so a session token presented to a relay is rejected. The test suite asserts this specifically, because it is exactly the sort of thing that quietly stops being true.

Relay enrollment codes#

One-time codes that let a relay register itself in the directory. Generate one on Relays, hand it to the relay as KINO_ENROLL_CODE, and it is consumed the moment the relay successfully enrolls.

The code is the entire credential, so treat it like a password — but its blast radius is small: the worst a stolen code does is add a relay you did not intend to the directory. It grants no access to any account or machine. The controller also refuses to enroll a URL whose /healthz it cannot reach, so a code cannot be burned on a relay that is not actually running.

Static relay tokens#

A relay can be configured with RELAY_TOKEN: one shared secret, compared in constant time, no controller involved. The same string then goes in the agent’s --token and the host’s Relay token field in the manager.

Simple, and right for a personal relay. But it is one password shared by everything that uses that relay, it never expires, and rotating it means touching every client. Anything multi-user should use controller-minted tokens.

The two mechanisms coexist: a relay with both configured accepts either, which makes the static token a useful safety net while you set up enrollment.

Revocation#

To cut off… Do this Effect
One machine Delete it on Machines Refresh fails immediately; its current relay token dies within 24 h
One copy of the app Revoke that account key Immediate — checked on every request
Everything at once Rotate the Ed25519 key Every relay must re-enroll; every token becomes invalid
A relay Stop it, or let its health check fail Agents stop picking it; parked agents move on their next reconnect

There is no way to invalidate an already-issued relay JWT before it expires. That is the deliberate trade for relays that hold no state and need no connection to the controller.

Passwords#

Account passwords are hashed with Argon2, minimum 8 characters. Sign-in hashes a dummy password for unknown emails, so response timing does not reveal which addresses have accounts.