Auth & join tokens
tabbify-auth is the single signing authority for your office. It issues
Ed25519-signed JWTs in two flavors — user-auth tokens for login and
node-join tokens for mesh peers — and every other service validates against
it. It matters because join tokens are what decide who gets onto your
private mesh, and the token claims — not the peer — are what
the coordinator trusts.
Two token kinds
A user-auth token is minted on login (kind: auth). Its claims are sub,
iat, exp, jti; default TTL is 24h. It carries no network or tags.
A node-join token is minted for a peer joining the mesh (kind: join). It
carries network (the mesh network name) and tags — both authoritative, so
a node cannot self-assert what it is allowed to be. Default TTL is 1h.
Both are signed with one Ed25519 key. The public key is served as JWKS at
GET /v1/jwks; the kid in each JWT header points at the right key.
Issuing a join token
Join tokens are admin-only. The auth service runs inside your mesh — it is
not a public endpoint, and the coordinator and admins reach it over plain HTTP on
the internal network. You pass the network and the tags the peer is permitted to
claim, and the auth service bakes them into the JWT ($AUTH_URL below is the auth
service's internal address):
curl -X POST $AUTH_URL/v1/tokens/join \
-H 'Authorization: Bearer $AUTH_ADMIN_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"network":"alice","tags":["tag:user-alice"],"ttl":3600,"subject":"alice-laptop"}'
# {"token":"eyJ0eXAiOiJKV1Q...","jti":"019e5ac6-...","kind":"join","expires_at":1779643046}
AUTH_ADMIN_TOKEN guards the raw admin endpoints (register, join, revoke), compared
in constant time. Self-serve issuance is also live: the admin panel's your
network → Add a node mints a join token scoped to your network (the server
stamps the network + tags authoritatively), so you don't handle the admin token
directly.
Joining the mesh
The peer presents the token as Authorization: Bearer <JWT> when it registers.
In production you almost never type this by hand — the
one-command installer joins a machine to the mesh for you,
and the coordinator's address is baked into the joiner binary (zero-config),
so a join is just tabbify-mesh join --name supervisor-01 --join-token <JWT>.
For a local dev run you point the joiner at your own coordinator and pass the token explicitly:
tabbify-mesh join --name supervisor-01 \
--coordinator http://127.0.0.1:8888 \
--insecure-no-mtls \
--join-token $MESH_JOIN_TOKEN
A few things worth keeping straight:
--coordinatoris a plain HTTP base URL. In production it is baked in, so you omit the flag entirely; the override above is only for local dev.- The
--tls-cert/--tls-key/--tls-caflags (omitted here) are peer-to-peer mTLS for the mesh control plane — they secure the joiner ↔ coordinator transport, not the coordinator's endpoint protocol. For a local smoke test you skip them with--insecure-no-mtlsagainst a coordinator launched the same way.
When the coordinator is started with AUTH_URL set, it calls back to validate
every join (POST $AUTH_URL/v1/validate over plain HTTP), then reads network
and tags from the token claims, not the request. This is the deliberate fix
for ACL spoofing: a node's own --tag flags are ignored once a token is in play.
curl -X POST $AUTH_URL/v1/validate \
-H 'Content-Type: application/json' -d '{"token":"eyJ0eXAiOiJKV1Q..."}'
# {"valid":true,"subject":"alice-laptop","network":"alice","tags":["tag:user-alice"],"kind":"join","exp":1779643046}
/v1/validate always returns 200 — even for garbage. Validation is a result
(valid: true|false), not an error, and it checks signature, exp (no leeway),
and revocation.
Revocation
Every issued token's jti is recorded. Revoking is instant:
curl -X DELETE $AUTH_URL/v1/tokens/019e5ac6-ec8d-7b32-bfd0-205066b12dc2 \
-H 'Authorization: Bearer $AUTH_ADMIN_TOKEN'
# {"jti":"019e5ac6-...","revoked":true} → next /v1/validate returns {"valid":false}
Revocation is enforced at /v1/validate time. Services that verify locally via
JWKS accept eventual consistency until they re-check or the (short) TTL expires.
Honest about scope
This is P1. Mesh coordinator integration is live — it validates join tokens on
register today. Without AUTH_URL set on the coordinator (the dev / E1 escape
hatch), there is no validation and --tag flags are trusted from the request.
Local JWKS validation in hot-path services, key rotation, and self-serve
registration are deferred. See self-hosting a node to wire a
peer end to end.