TILLNOTARY · TOKENS

Service tokens.

GA

Machines 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.

01Format

Shown once, stored hash-only

A token is tnot_ followed by 32 hex characters:

text
tnot_9f2a41c8b06de7b03a5d1c88f4e20b17
└┬─┘ └───────────── 32 hex ─────────────┘
 prefix — greppable, so a leaked token is findable in code review
          and secret scanners can match it with zero false positives

The 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.

02Minting

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:

bash
$ 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.503Z

Provisioning 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.

03Scopes

The ladder: read < submit < admin

Scopes are strictly ordered — each tier includes everything below it. Mint the lowest tier that does the job:

ScopeAllowsTypical holder
readCheckpoints, signed notes, leaves, inclusion & consistency proofs, witness status, evidence export — everything needed to verify, nothing that changes state.Monitors, consistency heartbeats, auditors
submitEverything read allows, plus appending entries (hash submissions and raw attestations up to 4096 bytes).CI pipelines, build signers, application backends
adminEverything 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:

json
# 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" }   # 404
04Pinning

Pin 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.

05Lifecycle

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/tokens or with tilldev notary tokens revoke <id>; the next request with that token fails 401 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.
06Handling

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:

bash
# 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.gz
If a token lands in a repo
Treat it as burned the moment it is committed — revoke and re-mint, even if the commit was never pushed. The tnot_ 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.

07Limits

Rate limits & Retry-After

Every request counts against the first limit; the others apply per token or per witness on top of it.

LimitRateCounted per
every request300 per 10 sClient IP address, or the calling zone for requests from Cloudflare Workers
reads200 per 10 sToken (reads without a token count only against the first limit)
submissions50 per 10 sToken
administration30 per 60 sToken
witness requests300 per 10 sWitness key

Going over answers 429 RATE_LIMITED with a Retry-After header in seconds:

http
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.


Wire the token into the CLI or the Node SDK. Logs themselves are managed at /<org>/notary/logs.