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 asAuthorization: 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.