StellaOps Registry
Audience: operators configuring registry access for scans and deployments, and integrators that pull from private or mirrored registries through Stella Ops.
Scope note —
src/Registry/is one two-role service family.registry-webis the Stella-owned distribution data plane;registry-tokenis its bearer issuer and plan-admin role. They share the family database but no source edge: the contract crosses as a signed registry token. The Stella Registry runs on its own service name, whileregistry.stella-ops.localremains on the interim zot until SR-6 completes the alias cutover. The token service’s generated OpenAPI reference lives inregistry-token/.Direction (2026-08-03, ADR-041): the owner ruled that a Stella-owned product registry — the Stella Registry — is built in this family (
registry-webdata plane; this token service becomes its auth plane), and zot is demoted to demo-integration material. The stella-infra zot remains the interim plugin-publish target until the ADR-041 R6 cutover. See the registry topology truth doc for every registry-shaped surface and its single role, anddocs/implplan/SPRINT_20260803_004_Registry_stella_registry_service_program.mdfor program state.
Registry Token Service issues short-lived Docker registry bearer tokens for private or mirrored registries. It exchanges an Authority-issued access token for a registry-compatible JWT after enforcing plan and licence constraints, so registry credentials never need to be distributed to callers.
Responsibilities
- Validate Authority-issued caller identity and required scopes (default
registry.token.issue; configurable viaAuthority.RequiredScopes). - Authorize requested repository scopes against a plan catalogue. An Authority-issued
stellaops:planassignment is authoritative; only an absent claim falls back to configuredDefaultPlan, while an unknown claimed plan returnsplan_unknownwithout downgrade. - Block issuance for revoked licences (
stellaops:licenseclaim + configured deny list) and for disabled or unknown plans. - Mint registry tokens with a bounded lifetime (default 5 minutes, capped at 1 hour) signed by a local RSA key loaded from PEM or PFX.
Key endpoints
GET /token- Docker registry token exchange endpoint (authorization policyregistry.token.issue). Requires aservicequery parameter and one or morescopeparameters of the formrepository:<name>:<actions>(space-delimited per OAuth2; actions comma-separated, defaulting topull). On success returns JSON{ token, expires_in, issued_at, issued_token_type }whereissued_token_typeisurn:ietf:params:oauth:token-type:access_token. Returns RFC7807 problem responses: 400 for a missing/invalidservice, invalid scope, or no scopes; 403 when the service is not inRegistry.AllowedServicesor when plan/licence authorization fails./api/admin/plans- plan-rule administration group (authorization policyregistry.admin):GET /api/admin/plans- list all plan rules ordered by name.GET /api/admin/plans/{planId}- get a plan rule by ID (404 if absent).POST /api/admin/plans- create a plan rule (201 withLocation; 400 on validation failure; 409 on name conflict).PUT /api/admin/plans/{planId}- update a plan rule; requiresversionfor optimistic concurrency (200; 400/404; 409 on version or name conflict).DELETE /api/admin/plans/{planId}- delete a plan rule (204; 404 if absent).POST /api/admin/plans/validate- dry-run validation of a plan rule without saving; optionally evaluates suppliedtestScopes.GET /api/admin/plans/audit- paginated plan-change audit history (optionalplanId,page,pageSize;pageSizeclamped to 1-100,pagefloored to 1).
GET /healthz- liveness probe (health checkself).GET /openapi/v1.json- anonymous OpenAPI discovery document (mapped viaMapOpenApi().AllowAnonymous()so the gateway aggregator can read it without a tenant header).- Build-info endpoint - image/provenance metadata for the operator verify aggregator (mapped via
MapBuildInfoEndpoint, module nameregistry-token).
Note: both
registry.adminandregistry.token.issueare defined in the canonical Authority scope catalogue (StellaOpsScopes.RegistryAdminandStellaOpsScopes.RegistryTokenIssue).
Code locations
- Service host:
src/Registry/StellaOps.Registry.TokenService/Program.cs - Token issuer:
src/Registry/StellaOps.Registry.TokenService/RegistryTokenIssuer.cs - Plan evaluation:
src/Registry/StellaOps.Registry.TokenService/PlanRegistry.cs - Scope parsing:
src/Registry/StellaOps.Registry.TokenService/RegistryScopeParser.cs - Options:
src/Registry/StellaOps.Registry.TokenService/RegistryTokenServiceOptions.cs - Plan admin API:
src/Registry/StellaOps.Registry.TokenService/Admin/PlanAdminEndpoints.cs,Admin/AdminModels.cs,Admin/PlanValidator.cs,Admin/IPlanRuleStore.cs - Persistence:
src/Registry/StellaOps.Registry.TokenService/Admin/Persistence/(PostgresPlanRuleStore.cs,RegistryTokenPlanRuleDataSource.cs,RegistryTokenPersistenceExtensions.cs) - Migrations:
src/Registry/StellaOps.Registry.TokenService/Migrations/001_initial_schema.sql,002_reserve_plan_allowlist.sql - Metrics:
src/Registry/StellaOps.Registry.TokenService/Observability/RegistryTokenMetrics.cs - Tests:
src/Registry/__Tests/StellaOps.Registry.TokenService.Tests
Configuration
- File:
etc/registry-token.yaml(the relative location is resolved to an absolute path before loading; optional, no reload-on-change; deployment environment variables are re-applied afterwards and win). - Configuration section:
RegistryTokenService(YAML root keyregistryTokenService). - Environment variable prefix:
REGISTRY_TOKEN_. - Key options (
RegistryTokenServiceOptions):Authority-Issuer,MetadataAddress,RequireHttpsMetadata(defaulttrue),Audiences,RequiredScopes(default["registry.token.issue"]).Signing-Issuer, optionalAudience(defaults to the requested registry service),KeyPath(PEM/PFX),KeyPassword, optionalKeyId(kid),Lifetime(default00:05:00, must be between 1 second and 1 hour).Registry-Realm(absolute URI),AllowedServices(empty list permits any service).Plans- plan catalogue (Name+Repositories, each withPatternsupporting*wildcard andActionsdefaulting topull).RevokedLicenses- deny list of revoked licence/customer IDs (normalized lowercase).DefaultPlan- plan used when nostellaops:planclaim is present.Postgres- durable plan-rule persistence (ConnectionString,SchemaName, etc.). When a connection string is supplied, plans are served from PostgreSQL and the in-configPlanslist is not required; when absent (outside theTestingenvironment) startup fails unless staticPlansare provided.
Persistence
- When
RegistryTokenService:Postgres:ConnectionStringis set, plan rules and audit history are stored in PostgreSQL (default schemaregistry_token). - Auto-migration runs on startup via
AddStartupMigrations(moduleRegistry.TokenService); migration SQL is embedded in the service assembly. Forward-only per ADR-004. - Tables (baseline
001_initial_schema.sql, compatibility contract002_reserve_plan_allowlist.sql):plan_rules(id,name,description,enabled,repositoriesjsonb,allowlistjsonb,rate_limitjsonb,created_at,modified_at,version), with a unique index onlower(name). The physicalallowlistcolumn is reserved and unused: create/import write[], update cannot write it, it remains read-only in responses, and authorization never consults it.plan_audit(audit_seqbigserial,plan_id,action,actor,timestamp,summary,previous_version,new_version).
- On an empty durable store, the first-boot catalogue importer copies the shipped plan catalogue once and records a durable marker. Later file edits never overwrite operator-managed rows; token authorization also never falls back from PostgreSQL to YAML.
- Without a connection string, the service runs with the static in-config plan catalogue (the
Testingenvironment supplies anIPlanRuleStoreexplicitly).
Observability
- Audit events are emitted to the Timeline service via unified audit emission (
AddAuditEmission). - OpenTelemetry metrics (meter
StellaOps.Registry.TokenService):registry_token_issued_total- counter of issued tokens, tagged byplan.registry_token_rejected_total- counter of rejected requests, tagged byreason(e.g.license_revoked,plan_unknown,plan_disabled,scope_not_permitted,no_scopes_requested).
- ASP.NET Core and HttpClient tracing instrumentation plus runtime metrics are registered via
AddStellaOpsTelemetry.
Implementation Status
Current Objectives
- Maintain deterministic behaviour and offline parity across releases
- Keep documentation, telemetry, and runbooks aligned with latest sprint outcomes
Epic Milestones
- Epic 10 – Export Center: signed registry token bundles for mirror/Offline Kit workflows (planned)
- Epic 14 – Identity & Tenancy: tenant-aware scope validation, revocation, audit trails (planned)
Core Capabilities
- Docker registry token exchange with Authority validation (resource-server JWT auth)
- Plan/repository scope enforcement via the
stellaops:planclaim (orDefaultPlan) against configured or PostgreSQL-backed plan rules - Short-lived JWT tokens (default 5 minutes) signed by a local RSA key; emitted access claim mirrors the requested repository scopes, and the
stellaops:licenseclaim is propagated when present - Revocation support via the configured deny list matched against the
stellaops:licenseclaim - Plan-rule admin API (CRUD, dry-run validate, audit history) backed by PostgreSQL with optimistic-concurrency versioning
Technical Decisions
- Token lifetime bounded to 5 minutes to minimize exposure window
- Local RSA key signing avoids external dependencies
- Plan catalogue enforcement ensures license compliance
- Integration with Authority for caller identity and scope validation
Coordination Approach
- Review
AGENTS.mdbefore starting new work. - Sync with cross-cutting teams via the sprint files under
../../implplan/SPRINT_*.md. - Track follow-ups in
src/Registry/TASKS.md.
Related docs
- Architecture — full design, token-exchange flow, and persistence detail.
- Operations: Token Service — deployment, key management, and runbooks.
