Skip to content

Free tier credentials

One credential type. One scope. One way to mint. Free and Mint Pack customers hold a single shared-secret bearer that authorises every mint they’re entitled to. There’s no permission-scope surface — that ships only on Dev tier.

The one bearer

PropertyValue
What it isAn opaque 32-byte random secret, base64url-encoded.
Where it livesWorkers KV namespace mint-free-state-prod at key token:<sha256-of-bearer>. The plaintext is shown to you once; we store only the hash.
What it authorisesPOST mint.stackrunner.dev/<ttl>/v1/free/issue (sign a cert) and POST mint.stackrunner.dev/v1/free/revoke (revoke a cert you previously minted; revoke is TTL-less).
ScopeThe entire Free Pack / Mint Pack quota for your handle. There is no per-CSR or per-purpose narrowing.
RotationNone today. Email support@ if it leaks; we revoke the hash and reissue.
Quota25 lifetime certs (Free Pack). Mint Pack lifts the cap; same bearer.

The bearer is the only credential the mint-free plane reads. It is also the only credential you, as a Free customer, hold — there is no birth cert, no scoped credential, no Teller surface, no Firebase SSO past signup. See “What’s not here on Free” below.

How you get it

You sign up on the dashboard with Google (or GitHub) SSO; the dashboard calls the bearer-claim-free Cloud Function exactly once for your handle. That CF generates the bearer, hashes it, writes the hash into KV under token:<hash>, marks bearer_claimed_at on your Firestore customer record, and returns the plaintext to the browser only.

The same record can never be claimed twice; a second call returns 409 already_claimed. If you lose the bearer, the only recovery is operator-initiated rotation.

Terminal window
# After dashboard signup — copy the bearer from the modal.
export STACKRUNNER_BEARER='<your-43-char-bearer-from-the-claim-modal>'
export STACKRUNNER_HANDLE=your-handle

How you use it

The bearer goes in the Authorization header on every mint. The TTL is the first path segment on mint.stackrunner.dev/ — one of {1h, 1d, 7d, 14d, 30d}. The worker rejects URL-vs-body mismatch with 400 ttl_mismatch. On *.workers.dev (dev/test) the worker falls back to body.ttl.

Terminal window
openssl ecparam -name prime256v1 -genkey -noout -out leaf.key
openssl req -new -key leaf.key -out leaf.csr \
-subj "/CN=${STACKRUNNER_HANDLE}.leaf.example"
curl -fsS \
-H "Authorization: Bearer ${STACKRUNNER_BEARER}" \
-H "Content-Type: application/x-pem-file" \
--data-binary @leaf.csr \
https://mint.stackrunner.dev/7d/v1/free/issue \
| jq -r .cert_pem > leaf.crt

That’s the public contract — raw CSR PEM body, no JSON envelope. If you’d rather send the envelope (for the version lever, or because your tooling already wraps requests), set Content-Type: application/json and POST {"version":"v1","csr_pem":"..."} instead; the response is identical.

The worker hashes the bearer, looks the hash up in KV, confirms the handle binding, enforces the per-minute rate limit (5 certs/min on Free Pack) and the 25-certs-total lifetime cap (with a per-TTL ceiling of 25 — same as the total, so you can spend the whole pack on a single TTL bucket), then proxies to the mint-free Cloud Function for signing.

To revoke:

Terminal window
curl -fsS \
-H "Authorization: Bearer ${STACKRUNNER_BEARER}" \
-H "Content-Type: application/json" \
-d '{"version":"v1","serial":"3e:5f:..."}' \
https://mint.stackrunner.dev/v1/free/revoke

(/v1/free/revoke is not TTL-scoped, so it doesn’t ride a <ttl>. path segment. Bare mint.stackrunner.dev/v1/free/revoke works.)

What’s NOT here on Free

The following exist on Dev tier and are absent on Free. They are not feature-flagged off — the routes literally don’t exist in the Free-plane worker. Trying to call them returns 404 bad_path.

CapabilityFree PackDev tier
POST /v1/sign-leaf — scoped CSR mint, leaf Subject overridden❌ no route
POST /v1/cross-sign — preserve CSR Subject + SANs in the issued leaf❌ no route
Per-credential rotation / revocation without rotating the master bearer✅ (scoped credentials are independent)
Birth cert (your own mTLS leaf for service-to-service mTLS)
Teller projection surface (teller.stackrunner.dev/<handle>/issue)
EST (RFC 7030) enrollment envelope (est.stackrunner.dev)✅ (via scoped credential)
Per-customer dedicated intermediate CA❌ shared intermediate-freedev-<handle>-intermediate

If you need any of the above, you need Dev tier. The bearer claim flow doesn’t gate against tier directly — the routes themselves are the gate.

Rotation, revocation, recovery

  • Bearer rotation: not self-serve today. Email support@ — we revoke the existing hash in KV and reissue. Cuts off in-flight callers; coordinate.
  • Cert revocation: any cert you minted, via the /v1/free/revoke endpoint above. Revocation is published in your CRL at pki.stackrunner.dev/<handle>/intermediate.crl within ~1 minute.
  • Handle compromise: if you suspect the bearer is in the wrong hands, revoke any valuable certs immediately, then ask for rotation. The bearer can’t mint past your lifetime cert cap, but it can burn your quota.

Operational details (for the curious)

  • The mint-free-state-prod KV namespace is shared across all Free Pack handles; entries are independent (token:<hash> is keyed by the secret, not by handle).
  • The bearer record stores {handle, tier?, created_at}. tier is informational; the worker’s enforcement is per-handle from KV + per-handle from D1 rate-limit.
  • Quota counters live in KV at quota:<handle>:<yyyy-mm> and quota:<handle>:<yyyy-mm>:ttl-<bucket>. The nightly quota-reconcile Cloud Function reconciles them against audit_events in Firestore and writes monthly snapshots.
  • The bearer never appears in audit logs in plaintext — only the hash + the resolved handle.

See also