Service tokens
Long-lived svc:* JWTs and where they live.
Three service tokens are in production. All have iss=https://auth.omg.dev,
aud=vibes-infra, and a 1-year TTL.
| Sub | Used by | Lives in | Purpose |
|---|---|---|---|
svc:convex | Convex actions | Convex env (VIBES_INFRA_SERVICE_TOKEN) | Server-side calls into infra (sandbox, deploys) |
svc:worker | CF Worker vibes-router | Worker secret (VIBES_INFRA_SERVICE_TOKEN) | KV-miss fallback to /v1/deploys-internal |
svc:ci | GitHub Actions e2e | GH secret VIBES_API_KEY (env: e2e) | Tier 2/3 tests against real infra |
A backup of every token lives in .env.production (gitignored) under
VIBES_INFRA_SERVICE_TOKEN_CONVEX, _WORKER, and _CI. Each line carries
the jti and expiry as a comment so a quick grep shows whether you're
about to need a rotation.
When to rotate
- Routine: 60 days before expiry. The
revoke.ts listcommand on the auth box printsEXPIRINGfor tokens within 30 days; grep for that in ops review. - Compromise: any token leak (committed to git, sent in an email, etc.)
→ rotate immediately and revoke the leaked
jti. The denylist propagates in ~30s.
See Auth → Rotating a service token for the step-by-step.
Why on-box minting only
The mint script is bun apps/auth/scripts/mint.ts running on the auth
box; there is no HTTP mint endpoint. Reasoning:
- A network-reachable mint endpoint requires an admin bearer, which would itself become the highest-value secret in the stack.
- On-box CLI access is gated by SSH key — no key, no mint.
- Compromise blast radius is limited to whatever lateral movement got someone shell on the auth box, which is also where the JWKS private key lives, so the bar is the same either way.