Dev tier credentials
Two stackrunner-issued bearers, one X.509 birth cert, one Firebase ID token, and (where Teller is in scope) one Teller credential. This is the credential surface a Dev-tier account owner works with day-to- day. It’s intentionally fatter than Free: Dev unlocks a second class of credential (“scoped credentials”) that gates the per-CSR cross-sign surface, plus a real X.509 birth cert for mTLS between your own services.
The credential surface at a glance
| # | Credential | Storage | Used for | Issued by |
|---|---|---|---|---|
| 1 | Master mint bearer (KV bearer) | Workers KV mint-paid-state-prod at token:<sha256> | POST /<handle>/mint (the legacy mint flow) | bearer-claim-paid CF — claimed once via dashboard |
| 2 | Scoped credential (permission-scope bearer) | Projection D1 machine_permission_credentials.credential_hash | POST /v1/sign-leaf and POST /v1/cross-sign (forwarded as X-Mint-Credential) | scripts/seed_signing_cert_bearer.py today (operator); dashboard-managed later |
| 3 | Birth cert | Customer-held P-256 key + leaf cert; we hold only the public key + fingerprint | Customer’s own mTLS between their services. Not used to talk to stackrunner. | birth-cert-mint CF, one-time per customer |
| 4 | Firebase ID token | Browser session | Dashboard SSO; gates bearer-claim-paid, birth-cert-mint, billing actions | Firebase Auth (Google / GitHub OAuth) |
| 5 | Teller credential (when Teller in scope) | Projection D1 machine_permission_credentials (same table as #2, different permission rows) | POST teller.stackrunner.dev/<handle>/issue — projection-layer token issuance | scripts/seed_teller_bearer.py (operator) |
Credentials #2 and #5 share a table by design — both are
permission-scoped, both ride as X-Mint-Credential /
X-Teller-Credential, both hash on the way in. The distinction is
which permission rows they bind to (signing-cert junction vs. teller
projection junction).
1. The master mint bearer (KV bearer)
This is the closest analogue to the Free Pack bearer — opaque 32-byte secret, account-rooted, claimed once. The differences from Free:
- It signs against your dedicated per-customer intermediate CA
(
dev-<handle>-intermediate, KMS-backed), not the shared Free intermediate. - The route shape is
mint.stackrunner.dev/<ttl>/<handle>/mint, notmint.stackrunner.dev/<ttl>/v1/free/issue. Per-handle scope is in the path after the TTL prefix. - TTL allowlist is wider:
{1h, 1d, 7d, 14d, 30d, 90d}— Dev tier adds90dfor the programmable-TTL case. - Quota: 5,000 leaves/month, batch up to 100 CSRs per call, all-or-nothing batch semantics.
Get it (one-time)
1. Sign in to the dashboard with Google or GitHub.2. Click "Claim mint bearer".3. Copy the plaintext from the modal — it appears once. We store only the SHA-256 hash.The dashboard calls the bearer-claim-paid Cloud Function, which
mints 32 random bytes, hashes them, writes
{handle, tier, created_at} to KV under token:<hash>, marks
bearer_claimed_at on your Firestore customer record, and returns
the plaintext to the browser only. A second claim attempt returns
409 already_claimed.
Use it
export STACKRUNNER_BEARER='<your-43-char-bearer-from-the-claim-modal>'export STACKRUNNER_HANDLE=<your-handle>
openssl ecparam -name prime256v1 -genkey -noout -out leaf.keyopenssl 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/${STACKRUNNER_HANDLE}/mint \ | jq -r '.certs[0].cert_pem' > leaf.crtSame TTL-in-URL contract as Free: URL subdomain wins, body ttl
either matches it or is omitted; mismatch → 400 ttl_mismatch.
For multi-CSR batch minting (the lever raw PEM doesn’t expose), keep using the JSON envelope:
curl -fsS \ -H "Authorization: Bearer ${STACKRUNNER_BEARER}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg c1 "$(cat leaf1.csr)" --arg c2 "$(cat leaf2.csr)" \ '{version:"v1", csr_pems:[$c1,$c2]}')" \ https://mint.stackrunner.dev/7d/${STACKRUNNER_HANDLE}/mintUp to PAID_BATCH_MAX = 100 CSRs per call, all-or-nothing.
Why not just use this for everything?
The master bearer is fine for an operator running mints by hand. It gets uncomfortable the moment you want to:
- Hand a credential to a single CI pipeline that should only mint short-lived certs and never with a non-default Subject.
- Rotate the credential a build farm uses without rotating the one a device fleet uses.
- Revoke one team’s mint access without bricking everyone else.
That’s what scoped credentials are for.
2. Scoped credentials (the permission-scope bearer)
The reason Dev tier exists as a separate plane. A scoped credential
is a bearer bound to a specific (machine_permission, signing_cert, mode) triple in the projection D1.
The permission surface
machine_permissions -- "permission objects" └── machine_permission_signing_certs -- which signing certs may issue under this permission, in which modes └── signing_certs -- the actual signing cert (KMS-backed) the leaf is chained to └── machine_permission_credentials -- bearers bound to this permission (sha256 hashed, one row per credential)A given account can have many machine_permissions. Each permission
can be junctioned to one or more signing_certs, with modes being a
comma-separated subset of {sign_leaf, cross_sign}. Each permission
can have many active credentials — independent rotation.
When you call /v1/sign-leaf or /v1/cross-sign, the request body
names a cert_id; the mint-crosssign Cloud Function:
- Hashes the
X-Mint-Credentialbearer. - Looks up
machine_permission_credentials.credential_hash→machine_permission_id. - Checks
machine_permission_signing_certsfor a row with that(machine_permission_id, cert_id)and the requested mode inmodes. - Pulls the KMS key reference from
signing_certs.kms_key_refand signs.
Mismatch at any step → 403. The credential doesn’t own the cert;
the permission does, and the credential is just one of N proofs of
holding the permission.
Seed one (operator-side today)
Scoped-credential management is operator-driven via
scripts/seed_signing_cert_bearer.py until the dashboard surface
ships. Output is a fresh bearer written to
.creds/<handle>-crosssign-bearer.txt (mode 0600, gitignored).
# Seed a credential for handle `apexrunner` with both modes:scripts/seed_signing_cert_bearer.py apexrunner --modes=sign_leaf,cross_sign
# Output:# .creds/apexrunner-crosssign-bearer.txt ← plaintext bearer# D1 row in machine_permission_credentials (sha256 hashed)The script is idempotent on the permission + junction (re-runs reuse
the existing machine_permission and junction), but always writes a
fresh credential row. To rotate, run it again and revoke the previous
credential by revoked_at timestamp.
Use a scoped credential — /v1/sign-leaf (mode A)
Override the CSR’s Subject; the issued leaf carries
<handle>.leaf.stackrunner.dev. Same profile as the master-bearer
mint, just signed by a chosen cert_id.
export CRED=$(cat .creds/apexrunner-crosssign-bearer.txt)CERT_ID=$(curl -fsS -H "Authorization: Bearer $(jq -r .api_token .creds/cloudflare.json)" \ "https://api.cloudflare.com/client/v4/accounts/$(jq -r .account_id .creds/cloudflare.json)/d1/database/abca2f42-972b-4b88-8b0a-13959c42f749/query" \ -d '{"sql":"SELECT cert_id FROM signing_certs WHERE label = ?","params":["apexrunner-intermediate"]}' \ | jq -r '.result[0].results[0].cert_id')
curl -fsS \ -H "Authorization: Bearer ${CRED}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg id "$CERT_ID" --arg csr "$(cat leaf.csr)" \ '{version:"v1", cert_id:$id, csr_pems:[$csr], ttl:"24h"}')" \ https://mint.stackrunner.dev/v1/sign-leaf \ | jq -r '.certs[0].cert_pem' > scoped-leaf.crtUse a scoped credential — /v1/cross-sign (mode B)
Preserve the CSR’s Subject + SANs verbatim in the issued leaf. Useful when the device or service expects a specific identity and you just need it cross-signed under a stackrunner-managed cert.
curl -fsS \ -H "Authorization: Bearer ${CRED}" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg id "$CERT_ID" --arg csr "$(cat leaf.csr)" \ '{version:"v1", cert_id:$id, csr_pems:[$csr], ttl:"7d"}')" \ https://mint.stackrunner.dev/v1/cross-sign \ | jq -r '.certs[0].cert_pem' > cross-signed-leaf.crtThe credential is only honoured for the modes its junction row
allows. A mode=sign_leaf credential calling /v1/cross-sign gets a
403 mode_not_allowed.
Where scoped credentials are also used
- EST enrollment (
est.stackrunner.dev/.well-known/est/<batchId>/simpleenroll): same scoped credential rides as theAuthorization: Bearerheader; the EST worker forwards it asX-Mint-Credentialto mint-crosssign. The EST<batchId>is themachine_permission_id. - Teller projection (
teller.stackrunner.dev/<handle>/issue): separate Teller-scoped credential, same table, different permission rows.
3. Birth cert
The customer’s own mTLS leaf cert. Not a stackrunner-side credential at all — you hold the private key, we never see it. Use it for service-to-service mTLS between your own systems.
| Property | Value |
|---|---|
| Key | EC P-256, generated in the browser via WebCrypto. |
| Public key delivery | SPKI DER, base64-encoded, sent to birth-cert-mint CF over the dashboard session. |
| Signing | The CF signs a 1-year cert against your dev-<handle>-intermediate. |
| Lifecycle | One-time per customer — birth_cert_fingerprint on your Firestore record locks it. Renew = email support@. |
| Used by stackrunner? | No. Bearer #1 / #2 authenticate you to us. The birth cert is yours, for your own mTLS. |
(mTLS to stackrunner — i.e. presenting your birth cert at the mint.stackrunner.dev edge instead of a bearer — ships in Phase 1.5 once Cloudflare API Shield costs pencil. Currently bearer-only at the edge.)
4. Firebase ID token
Standard Google / GitHub OAuth flow. Browser-only, short-lived. Authenticates you to:
- The dashboard SPA itself.
bearer-claim-paid(one-time, to claim bearer #1).birth-cert-mint(one-time, to issue the birth cert).- Billing portal redirects (Stripe).
Not used to sign certs. Not equivalent to any of the bearers above.
5. Teller credential (when in scope)
Separate scoped-credential, lives in the same
machine_permission_credentials table as #2 but bound to
projection-layer permissions (issued in
machine_permission_mint_batch_domains) rather than signing-cert
junctions. Used by Teller’s projection-issue endpoint at
teller.stackrunner.dev/<handle>/issue. Seeded today via
scripts/seed_teller_bearer.py.
Out of scope of mint-side flows; documented here so the surface is complete.
Choosing the right credential
| You want to… | Use |
|---|---|
| Mint a cert by hand, account-wide quota, your own Subject auto-chosen | #1 (master bearer) |
Hand a credential to a CI pipeline that should mint only with a specific signing cert and only sign_leaf mode | #2 scoped credential, modes=sign_leaf |
| Cross-sign a CSR that needs its Subject + SANs preserved | #2 scoped credential, modes=cross_sign |
| Stand up an EST client (OpenSSL, libest, embedded TLS) | #2 scoped credential as bearer to est.stackrunner.dev |
| Authenticate your services to each other (your own mTLS) | #3 birth cert |
| Sign into the dashboard | #4 Firebase ID |
| Issue Teller projections (JWT/JWE/COSE/SPIFFE) | #5 Teller credential |
Rotation, revocation, recovery
- Master bearer (#1): not self-serve today. Email
support@; we delete the KV record and reissue. Cuts off in-flight callers using the old plaintext — coordinate. - Scoped credentials (#2, #5): rotate by re-running the seed
script; revoke by setting
revoked_aton themachine_permission_credentialsrow. Other credentials on the same permission keep working — that’s the whole point of having them scoped. - Birth cert (#3): customer-managed key; if compromised, rotate
your own services first, then revoke the cert via your CRL
(
revoke-paidCF) and emailsupport@to clear thebirth_cert_fingerprintflag so you can mint a new one. - Firebase (#4): standard Google/GitHub session controls. Sign out to invalidate.
See also
- Free tier credentials — what the Free / Mint Pack surface looks like (one bearer, no scoped class).
- Dev tier quickstart — end-to-end first cert with bearer #1.