Skip to main content
A registry may advertise an authentication method in its well-known document. Auth is advisory: it is never required, and it exists so Cyrnel can protect the registry’s private content and issue scoped tokens. A registry that doesn’t advertise auth is simply used anonymously. Two methods are understood; anything else is recorded as unsupported (ignored) until a setup request needs it, which then fails with 400.

API key (header)

  • name is the header Cyrnel will send the key under (trimmed). It is pinned at setup.
  • Query-parameter API keys are not supported: advertising "in": "query" (or anything other than "header") marks the method unsupported with the reason 'in' must be 'header'; query-param api keys are not supported.

OAuth2 client credentials

  • grantType may only be client_credentials (or omitted); anything else is unsupported.
  • tokenEndpoint must be an absolute http(s) URL.
  • scopes is an optional list of { id, description? }. It describes the scopes the registry offers; see Scope selection.

Credential scope

Credentials are attached only to requests within the registry’s origin and, if the base URL has a path, that path prefix. On cross-origin redirects they are stripped - Cyrnel re-evaluates scope at every hop.

Transport rules

Credentials are only sent over:
  • https, or
  • http where the resolved address is loopback, or
  • http where the resolved address matches a CIDR in CYRNEL_REGISTRY_AUTH_INSECURE_CIDRS.
Attach-time violations fail the request with 502 Registry authentication requires https; refusing to send credentials over plaintext http. Setup-time violations are 400 safety refusals (nothing stored). Local development over plaintext on loopback works out of the box.

Setup

Auth is configured either at add time (POST /registries with auth) or later via POST /registries/:id/auth: both validated against the live advertisement (fetched fresh, never from the stored snapshot):

Safety refusals (400, nothing stored)

Failure persistence (two paths)

Success returns auth: { type, status: "configured", tokenExpiresAt }.

Pinning & drift

headerName (apiKey) and tokenEndpoint (oauth2) are pinned from the advertisement at setup time. If the registry later changes what it advertises, Cyrnel keeps using the pinned values and logs a warning (prompting reconfiguration): it never silently adopts the drift. Drift log events: registry-auth-unsupported, registry-auth-unconfigured, registry-auth-type-drift, registry-auth-token-endpoint-drift (“pinned endpoint retained (reconfigure to adopt)”), registry-auth-header-drift (“pinned header retained (reconfigure to adopt)”).

Token lifecycle

  • Tokens are persisted encrypted in the registry_auth table and reused until their expiresAt is within 30 seconds of expiry (skew), then a fresh exchange happens.
  • The exchange is single-flight: concurrent first requests share one request.
  • Token requests are POST <tokenEndpoint> with a application/x-www-form-urlencoded body: grant_type=client_credentials, client_id, client_secret, and scope (space-joined selected scopes).
  • The response must include access_token (string); expires_in (seconds) defaults to 1 hour when absent; refresh_token is stored but never used: refresh is always a fresh client-credentials exchange.
  • A single 401 from the upstream after using an oauth2 token triggers one retry with a freshly exchanged token. apiKey auth never retries.
  • Signature for well-formed failures: non-2xx token response → 502 Registry oauth2 token endpoint responded with status N.; malformed → 502 with the parse reason.

Scope selection

  • Setting scopes to a subset of the advertised list is allowed and attached to the token request.
  • Omitting scopes requests the full advertised set (Cyrnel sends the space-joined advertised scope ids).
  • Any scope not advertised is a 400 safety refusal - a registry that doesn’t advertise scopes gets no scope parameter at all.
  • The current state is readable via GET /registries/:id/auth:
availableScopes is fetched live from the registry’s current advertisement on every read (never cached); configuredScopes is decrypted from stored config. authType may be null (nothing configured); tokenExpiresAt is null for apiKey.

Testing drift & scopes

The dev fixture can advertise values it does not enforce (CYRNEL_DEV_REGISTRY_DRIFT_AUTH=1) to exercise the pinning behavior, and it enforces its advertised scopes on the token endpoint (invalid_scope for unknown requested scopes). See Building a registry.
Last modified on August 25, 2026