Capability-Grant Authorization (Permission to Run, feature 078)

wbsp-api can authorize application hosting from issuer-signed capability grants (PTA + RTH) instead of only an identity check, and enforce the resource limits those grants carry. It ships disabled (observe mode) and must be explicitly switched on per cluster.

The two modes

ModeWhat wbsp-api does
observe (default)Verifies grants and logs what it would reject / cap / tear down — but changes nothing. Every deploy behaves exactly as before. The existing identity check stays authoritative.
enforceA missing/invalid/replayed grant refuses the deploy (403/409); the RTH's hard ceilings are applied as k8s container Resources + a namespace ResourceQuota/LimitRange; a revoked grant's app is torn down by the reconcile loop.

wbsp-api cannot make a deploy fail that would succeed today until enforce is enabled.

Environment variables (on the wbsp-api pod)

VariableMeaning
WBSP_GRANT_VERIFY_KEYSComma-separated kid:base64(ed25519 public key) entries — the public keys wbsp-api trusts to sign grants. Public keys only; never a private/secret key. Now the FALLBACK for any role without its own anchor.
WBSP_PTA_VERIFY_KEYSKeys trusted to sign permission-to-access (image access). Typically the catalogue website's.
WBSP_RTH_VERIFY_KEYSKeys trusted to sign hosting grants. Typically this platform's subscriber portal.
WBSP_CONTROL_VERIFY_KEYSKeys trusted to sign control tokens (the control plane below).
WBSP_GRANT_AUDIENCEThe audience every grant must name (aud). One value per cluster, by convention cluster:<operator-chosen id>. Defaults to WBSP_API_AUDIENCE.
WBSP_GRANT_MODEobserve (default) or enforce.

Notes:

  • With no verify keys at all, grant checks are disabled (pre-078 behavior).
  • Each per-role variable falls back to WBSP_GRANT_VERIFY_KEYS while unset, so a cluster that sets none of them behaves exactly as it did before the split. A role variable that IS set replaces the legacy set for that role — it never merges, because merging would leave the legacy signer able to mint the role you just separated.
  • WBSP_GRANT_MODE=enforce with any role unable to verify anything is refused at boot, naming the role — never a silent open gate.
  • Verification is offline (no JWKS fetch); if keys are configured but unloadable, verification fails closed.
  • Boot logs name each role's key source and the effective audience, so a rollout is checkable without redeploying: pta=WBSP_PTA_VERIFY_KEYS rth=WBSP_GRANT_VERIFY_KEYS (fallback) ….

Cutover procedure (per cluster)

  1. Enable observe: set WBSP_GRANT_VERIFY_KEYS (+ optionally WBSP_GRANT_AUDIENCE), leave WBSP_GRANT_MODE=observe, redeploy wbsp-api.
  2. Soak: run real deploys and watch the grant logger. Each deploy emits grant.observe would_allow or grant.observe would_reject reason=<x>. Confirm the would-decisions are correct against live traffic. Nothing is enforced yet.
  3. Separate the anchors and the audience — both are preconditions of enforce, not optional polish. Give each role its own keys, and give this cluster its own cluster:<id> audience, still under observe: a mismatch is a would-reject log line, not an outage. Prove each key with POST /api/v1/grants/verify (control-token authenticated; writes nothing).
  4. Flip to enforce: set WBSP_GRANT_MODE=enforce, redeploy wbsp-api. From now on invalid grants are refused and resource ceilings are applied.
  5. Reversible: set WBSP_GRANT_MODE=observe and redeploy to fall back at any time — no app redeploy required.

Do not skip step 3. Enforcing on a shared anchor and a shared audience is a gate that looks closed and is not: whoever can sign one role can sign them all, and a token minted for another cluster still names the same audience.

Control plane (control-token auth)

The issuing authority (website/HAP) calls these with a control token signed by a trusted key:

  • GET /api/v1/tenants/{tenant}/usage · GET /api/v1/tenants/{tenant}/apps · GET /api/v1/tenants/{tenant}/apps/{name}/usage — inventory, limits, threshold state. (Usage / estimated cost are "unavailable" until Phase 2 metering lands.)
  • POST /api/v1/grants/verify — verify-only: does this cluster accept a token, and as which role? Writes nothing, deploys nothing. The way an issuing authority proves its keys.
  • POST /api/v1/tenants/{tenant}/apps/{name}/enforce {action: shutdown} — stop an app (observe returns applied:false); {action: resume} — redeploy it from the platform's retained record, in either mode, taking deploy time and reporting no resulting state; throttle is 501.
  • DELETE /api/v1/grants/{jti} — revoke a grant. Enforce mode marks it revoked and the reconcile loop tears the app down; observe returns applied:false.

Platforms without a deployer

A cluster may run wbsp-api with WBSP_API_DEPLOYER=disabled (the appliance case) and grants configured. It boots: the verifier and control plane are live, and only the reconcile loop is skipped, with a WARN naming it. There, revocation is recorded but not executed — the response carries executed:false — until that platform supplies a remover; shutdown and resume answer 503 deployer_unavailable. This combination used to be fatal at boot.

What is NOT in this feature

  • Cost enforcement / metering — Phase 2 (rate card + the dormant wbsp.usage.* instruments).
  • Federation — anchors are per ROLE, not per issuer; issuer→tenant scoping and per-issuer revocation are deferred.
  • The token-minting side — issued by the WBSP website (or a HAP) per specs/078-permission-to-run/contracts/token-claims.md; not part of wbsp-platform. Running your own platform and want your own user/app management instead of wbsp.ai's? Build your own issuer — see Building an Issuing Authority.