Registry Token Service architecture

Audience: Operators deploying the Registry Token Service and integrators wiring Docker/OCI clients or the plan-administration API. Source: src/Registry/StellaOps.Registry.TokenService. Related: token-service operations runbook.

The OCI registry itself is not in this module. stellaops-registry is the third-party zot registry (ghcr.io/project-zot/zot-linux-amd64:v2.1.3, devops/compose/docker-compose.stella-infra.yml:126-128) — infrastructure, not Stella Ops source. This dossier covers only stellaops-registry-token, the service that issues that registry’s bearer tokens.

Overview

Registry Token Service is the Stella Ops component that issues short-lived Docker registry bearer tokens for private or mirrored registries. It is designed for offline/self-hosted operation and enforces plan/licence constraints before minting any registry token.

In addition to token exchange, the service exposes an authenticated Plan Administration API for managing the plan-rule catalogue (CRUD, dry-run validation, and audit history) that drives those authorization decisions.

The service surface is small and focused:

Authorization decisions are based on (a) Authority-issued identity token claims and (b) plan rules read from the durable PostgreSQL store (or, in non-durable test hosts, an explicitly registered in-memory store).

Primary responsibilities

Runtime components

Minimal API host

Cross-cutting integrations

Auth integration

Plan registry (token authorization rules)

Plan administration API (/api/admin/plans)

Plan administration storage

Token issuer

Observability

Request flow

  1. Docker/OCI client receives a 401 from the registry with a WWW-Authenticate: Bearer realm=...,service=...,scope=repository:... challenge.
  2. Client obtains an Authority token with the registry.token.issue scope (and any required sender constraints for the deployment).
  3. Client calls GET /token?service=<service>&scope=repository:<repo>:<actions> on Registry Token Service. The scope query may repeat or be space-delimited (OAuth2 style); each scope is repository:<name>:<comma-separated-actions>.
  4. Service validates:
    • service is present (and is allow-listed if Registry:AllowedServices is configured)
    • requested scopes parse correctly (type must be repository; at most 3 colon-separated segments; missing actions default to pull)
    • at least one scope is requested
    • caller plan/licence claims authorize all requested repository actions
  5. Service returns a JSON response containing the signed registry token.

Denial paths:

Token shape (Docker registry compatible)

The JWT header carries alg = RS256 and, when a key id is resolved (see Token issuer), a kid.

The issued JWT payload includes registered claims iss, aud, nbf, iat, exp, plus:

The GET /token HTTP response is a JSON envelope:

{
  "token": "<jwt>",
  "expires_in": 300,
  "issued_at": "<ISO-8601 UTC>",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

Configuration

Configuration is loaded from:

Key sections are defined by RegistryTokenServiceOptions (root section RegistryTokenService):

Durable plan-rule persistence is configured separately under RegistryTokenService:Postgres (at minimum ConnectionString; optional SchemaName, defaulting to registry_token). When Postgres persistence is configured, the host may start without any statically configured Plans; persisted plan rules become the canonical source for admin CRUD and token issuance. Without a connection string, the host requires at least one statically configured plan and refuses to start in non-test environments.

Roadmap / not-yet-implemented

References