Pinboard identifies callers with Passport, configured in PINBOARD_PASSPORT. Passport answers who the caller is; Pinboard decides what they may do.
| Surface | Credential | Required |
|---|---|---|
/registry/*, /metrics | Passport credential with pinboard:worker or pinboard:operator | Always |
/{namespace}/{id}/... | Passport credential, or an attested instance | Per namespace rule; open when no rules are configured |
/healthz, /readyz | None | Never |
| Pinboard → worker | Pinboard-Attestation, or Pinboard-Proxy-Secret | Always |
Sending a credential
By default every provider reads a bearer token:
curl https://pinboard.example.com/registry/workers \
-H "Authorization: Bearer $PINBOARD_OPERATOR_TOKEN"A provider can read a cookie or a named header instead (credential: {cookie: "name"} or {header: "name"}).
Resolution order:
- Providers run in config order,
staticTokenproviders first. - A provider with no credential in its location is skipped.
- A
jwtprovider skips a token whoseissis not itsissuer. - A
staticTokenprovider skips a token that matches none of its keys. - The first provider that accepts the credential names the principal. One that rejects it fails the request.
- No provider accepting means no credential: anonymous.
| Provider | Principal sub | Scopes | Claims |
|---|---|---|---|
staticToken | the key's name | the key's scopes | the key's claims |
jwt | token sub | token scope (space-separated), else the provider's scopes | token claims |
introspect | response sub | response scope | response claims |
Registry roles
| Scope | Routes |
|---|---|
pinboard:worker | register, heartbeat, release, tag, deregister, and any unknown /registry/* path |
pinboard:operator | health, workers, overview, stats, wind_down, drain, undrain, /metrics |
A credential needs the exact scope of the route; pinboard:operator does not imply pinboard:worker.
The pinboard claim
Registry access is scoped by the principal's claims.pinboard object:
| Field | Type | Required | Meaning |
|---|---|---|---|
namespaces | string[] | Yes, both roles | Namespaces (/threads) the principal may touch. ["*"] grants all. |
allowedBackendHosts | string[] | pinboard:worker | Hosts a worker may register as its advertiseUrl. |
allowedPorts | integer[] | No | Ports (1–65535) allowed in advertiseUrl. Absent: any port. |
hostnames | string[] | pinboard:worker in hostname-aware mode | Hostnames a worker may register. |
envs | string[] | pinboard:worker in hostname-aware mode | Envs a worker may register. |
Host rules in allowedBackendHosts, hostnames and envs:
| Rule | Matches |
|---|---|
* | anything |
10.0.0.0/8 | IPv4 addresses in the CIDR; IPv6 CIDRs are rejected |
.example.com | names ending in .example.com |
10.0.0.7, ::1 | that IP |
worker.internal | that exact name, case-insensitive |
For operators, absent hostnames and envs mean unrestricted.
{
"name": "fleet-worker",
"token": { "env": "PINBOARD_WORKER_TOKEN" },
"scopes": ["pinboard:worker"],
"claims": {
"pinboard": {
"namespaces": ["/threads"],
"allowedBackendHosts": [".workers.internal"],
"allowedPorts": [8000]
}
}
}Registry auth errors
| Status | Body | When |
|---|---|---|
401 | {"error":"unauthorized"} | No credential, or a provider rejected it |
403 | {"error":"forbidden"} | Credential lacks the route's scope, or its pinboard claim is malformed or missing namespaces |
Out-of-scope request bodies answer 403 with a specific message; see each endpoint.
Account ownership
Register stores the principal's sub as the worker's account. heartbeat, release, tag and deregister require the same sub, else 403 {"error":"forbidden"}. Operator routes judge only the claim scope, never the account.
Data-plane rules
The namespaces object in PINBOARD_PASSPORT turns on data-plane checks. Without it, every routed request passes.
{
"providers": [ ... ],
"namespaces": {
"/threads": { "allow": ["threads:write"] },
"/public": { "allow": "anonymous" },
"*": { "allow": "authenticated" }
}
}allow | Passes |
|---|---|
"anonymous" | everyone; a valid credential still rides as acct |
"authenticated" | any accepted credential |
string[] | a credential holding at least one listed scope |
- The exact namespace key wins over
"*". A namespace matching neither is denied. - Scopes starting
pinboard:are reserved; the config is rejected at boot. - A credential a provider rejects fails the request even under
"anonymous".
| Status | Body | When |
|---|---|---|
401 | {"error":"invalid credential"} | A provider rejected the credential |
401 | {"error":"authentication required"} | Rule is not anonymous and no credential was accepted |
403 | {"error":"namespace not allowed"} | No rule for the namespace and no "*" |
403 | {"error":"insufficient scope"} | Credential holds none of the listed scopes |
Instance attestation
An instance calling another instance through Pinboard proves its identity with its worker session instead of a Passport credential. See Call from an instance.
What workers receive
Pinboard strips every inbound Pinboard-* header and stamps its own. Workers trust only these.
| Header | On | Value |
|---|---|---|
Pinboard-Attestation | every forward, _pinned/stats, _wake, _alarm | HS256 JWS signed with the worker's registered secret |
Pinboard-Epoch | every forward, _wake, _alarm | placement epoch of the resolved row |
Pinboard-Proxy-Secret | _pinned/challenge, _alarm | the worker's registered secret |
Attestation JWS: header {"alg":"HS256","typ":"pinboard+jwt"}.
| Claim | Type | Meaning |
|---|---|---|
aud | string | the target worker's sessionId |
iat | integer | issue time, Unix seconds |
exp | integer | iat + 30 |
caller | string | {namespace}/{id} of an attested calling instance |
depth | integer | subrequest depth the caller sent |
acct | object | the data-plane principal: sub, scopes (without pinboard:*; omitted when empty), claims (without pinboard) |