Service tokens.
GAMachines talk to TillNotary with service tokens: tnot_-prefixed credentials scoped on a three-tier ladder, optionally pinned to a single log, and optionally set to expire. Mint them in the dashboard at /<org>/notary/tokens, with tilldev notary tokens create, or over the API.
Shown once, stored hash-only
A token is tnot_ followed by 32 hex characters:
tnot_9f2a41c8b06de7b03a5d1c88f4e20b17
└┬─┘ └───────────── 32 hex ─────────────┘
prefix — greppable, so a leaked token is findable in code review
and secret scanners can match it with zero false positivesThe service stores only the SHA-256 hash of the token — the plaintext exists in exactly two places, ever: the one-time reveal at mint, and wherever you put it. There is no “show token again” button, by design: if the plaintext is lost, mint a new token and revoke the old one. This also means a database read on our side cannot yield a usable credential.
Dashboard, CLI or API
Minting and revoking need an org owner or admin. In the dashboard, use the form at /<org>/notary/tokens. From a terminal, the CLI uses your tilldev login session (or TILLDEV_TOKEN), never a tnot_ token:
$ tilldev notary tokens create --name ci-submitter --scope submit --log acme-releases --expires 90
OK Created submit token ci-submitter pinned to acme-releases
id 2f6d0c7e-8a41-4b6e-9f0d-3c1e5b7a9d24
expires 2026-12-26T09:14:02.118Z
token tnot_9f2a41c8b06de7b03a5d1c88f4e20b17
* Copy the token now; it is shown once. Hand it over as TILLDEV_NOTARY_TOKEN.
# --quiet prints only the token, for piping straight into a secret store
$ tilldev notary tokens create --name ci-submitter --scope submit --log acme-releases --quiet | gh secret set TILLDEV_NOTARY_TOKEN
$ tilldev notary tokens
NAME PREFIX SCOPE LOG LAST USED EXPIRES STATUS ID
------------ --------- ------ ------------- --------- ---------- ------ ------------------------------------
ci-submitter tnot_9f2a submit acme-releases 2m ago 2026-12-26 active 2f6d0c7e-8a41-4b6e-9f0d-3c1e5b7a9d24
$ tilldev notary tokens revoke 2f6d0c7e-8a41-4b6e-9f0d-3c1e5b7a9d24
OK Revoked token 2f6d0c7e-8a41-4b6e-9f0d-3c1e5b7a9d24 at 2026-09-27T09:20:41.503ZProvisioning automation can call POST /api/notary/tokens and DELETE /api/notary/tokens/{id} with a TillDev API key that carries the notary.tokens.write scope; listing needs notary.logs.read. An API key pinned to particular logs can mint and revoke only tokens pinned to those logs. A tnot_ token can never mint another token, whatever its scope.
The ladder: read < submit < admin
Scopes are strictly ordered — each tier includes everything below it. Mint the lowest tier that does the job:
| Scope | Allows | Typical holder |
|---|---|---|
| read | Checkpoints, signed notes, leaves, inclusion & consistency proofs, witness status, evidence export — everything needed to verify, nothing that changes state. | Monitors, consistency heartbeats, auditors |
| submit | Everything read allows, plus appending entries (hash submissions and raw attestations up to 4096 bytes). | CI pipelines, build signers, application backends |
| admin | Everything submit allows, plus administration: creating logs, changing their settings, freezing them, attaching and detaching witnesses, and registering your own witnesses. A pinned admin token manages only its log. | Provisioning automation only; almost nothing should hold this |
The scope boundary is enforced with the standard error envelope:
# unknown, revoked or expired token (also on logs anyone can read)
{ "error": "Token is not valid: it is unknown, revoked or expired.", "code": "UNAUTHORIZED", "request_id": "5c0e7a2d-91b4-4f63-a8d2-6e1f3b9c7a40" } # 401
# valid token, insufficient scope (a read token attempting a submit)
{ "error": "Token scope does not allow submit on this log.", "code": "FORBIDDEN", "request_id": "8d41b6f0-2c7e-4a95-b3e1-0f6a9d2c5e87" } # 403
# valid token pinned to another log: the log answers as if it does not exist
{ "error": "No such log.", "code": "NOT_FOUND", "request_id": "e7a3c519-4b0d-4e2f-9c68-1d5b8f0a3e26" } # 404Pin a token to a single log
A token can be pinned to one log at mint time. To a pinned token, every other log in the org answers as if it does not exist — reads and writes alike return 404 NOT_FOUND, not 403, so the token cannot even enumerate what else is there.
Pinning is the safer default for a submitter. A CI pipeline that notarises release hashes needs exactly one log; if that pipeline is compromised, a pinned submit token bounds the damage to noise in one log — appends are the only mutation it permits, and appends to a verifiable log are, by construction, visible in the history forever, never a silent rewrite of it.
Expiry, revocation & rotation
- Set an expiry when you mint. Pick 30 days, 90 days or a year in the dashboard, or pass
--expires <days>to the CLI. After that moment the token fails exactly like a revoked one. - Revocation is immediate. Revoke from
/<org>/notary/tokensor withtilldev notary tokens revoke <id>; the next request with that token fails401 UNAUTHORIZED, even on a log anyone can read. A token that is sent must be valid; it is never quietly treated as no token. Nothing already appended is affected: history does not depend on the credential that wrote it. - Rotate with overlap, not downtime. Tokens are independent, so the rotation dance is simple: mint the replacement, deploy it, watch traffic move, revoke the old one. Nothing requires the two to be swapped atomically.
- Rotate on schedule and on suspicion. Put submitter tokens on a calendar rotation, and rotate immediately if a token may have been exposed — a CI log that printed env, a laptop that left the building. Minting is cheap; doubt is not.
- One token per workload. Separate tokens for separate pipelines cost nothing and make revocation surgical instead of disruptive.
Never commit a token
The CLI and SDK both read TILLDEV_NOTARY_TOKEN from the environment, which should be your only delivery mechanism — inject it from your CI secret store or your runtime’s secret manager:
# hand the token to the CLI via environment — never on the command line,
# never in a committed file
export TILLDEV_NOTARY_TOKEN=tnot_9f2a41c8b06de7b03a5d1c88f4e20b17
$ tilldev notary submit acme-releases --file dist/app-v2.4.1.tar.gztnot_ prefix exists precisely so secret scanners catch this, but a scanner finding it means it already existed somewhere it shouldn’t.Note that verification-only workflows may not need a token at all where a log’s read surface is open — and offline verification of an evidence bundle never involves a token, a network, or us.
Rate limits & Retry-After
Every request counts against the first limit; the others apply per token or per witness on top of it.
| Limit | Rate | Counted per |
|---|---|---|
| every request | 300 per 10 s | Client IP address, or the calling zone for requests from Cloudflare Workers |
| reads | 200 per 10 s | Token (reads without a token count only against the first limit) |
| submissions | 50 per 10 s | Token |
| administration | 30 per 60 s | Token |
| witness requests | 300 per 10 s | Witness key |
Going over answers 429 RATE_LIMITED with a Retry-After header in seconds:
HTTP/1.1 429 Too Many Requests
retry-after: 10
{ "error": "Too many requests. Retry after 10 seconds.", "code": "RATE_LIMITED", "request_id": "3b2f9c1e-6d4a-4f0b-8e57-a1c9d2e4f6b8" }Each log also has a daily submission cap, set on the log. Reaching it answers the same 429, with Retry-After counting down to when the rolling 24-hour window has room again. The CLI and the Node SDK retry network errors, 429 and 502–504 for you, waiting out Retry-After when it is short. Submitting the same value twice returns the original receipt, so a retried submission never logs twice.