StellaOps Registry Token Service
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 the token service, not a registry. Thestellaops-registrycontainer in the compose stack is third-party software, not Stella Ops source: it is the zot OCI registry (ghcr.io/project-zot/zot-linux-amd64:v2.1.3,devops/compose/docker-compose.stella-infra.yml:126-128, configdevops/compose/zot-config.json). This module only mints the bearer tokens that registry accepts; it never stores or serves images. The token-service container isstellaops-registry-token(its autogenerated OpenAPI reference lives inregistry-token/).
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 (
stellaops:planclaim, falling back to the configuredDefaultPlan, evaluated against plan/repository rules). - 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:
registry.adminis defined in the canonical Authority scope catalogue (StellaOpsScopes.RegistryAdmin).registry.token.issueis enforced as an authorization-policy scope string but is not (yet) declared as a constant inStellaOpsScopes; it is the hardcoded default forAuthority.RequiredScopes.
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 - Metrics:
src/Registry/StellaOps.Registry.TokenService/Observability/RegistryTokenMetrics.cs - Tests:
src/Registry/__Tests/StellaOps.Registry.TokenService.Tests
Configuration
- File:
etc/registry-token.yaml(loaded from../etc/registry-token.yamlrelative to the content root, optional, reload-on-change). - 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); the migration SQL is embedded in the service assembly. Forward-only per ADR-004. - Tables (
Migrations/001_initial_schema.sql):plan_rules(id,name,description,enabled,repositoriesjsonb,allowlistjsonb,rate_limitjsonb,created_at,modified_at,version), with a unique index onlower(name).plan_audit(audit_seqbigserial,plan_id,action,actor,timestamp,summary,previous_version,new_version).
- 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.
