stellaops-cli scope census and the operator-identity split
Verified against: commit 387a960602 (2026-09-05, SPRINT_20260722_016 AUTH-15). Re-verify:
# the ceiling, the YAML grant, the migration and the rollback agree (7 facts)
pwsh ./tools/scripts/test-targeted-xunit.ps1 \
-Project src/Authority/StellaOps.Authority/StellaOps.Authority.Plugin.Standard.Tests/StellaOps.Authority.Plugin.Standard.Tests.csproj \
-Class "*StellaOpsCliClientScopeCeilingTests*"
# the persisted row on a running estate is within the ceiling (reads authority.clients, not the YAML)
bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --verify
Why this document exists
stellaops-cli is the shipped human CLI client (authorization_code password refresh_token). Until 2026-09-05 it was also the estate’s manual-mint and break-glass identity: every operator script, runbook curl and QA harness that needed a human-bound token minted through it, and every grant those procedures ever needed was appended to its allowedScopes. The live authority.clients row carried 143 scopes; the shipped CLI can exercise 88. Two consequences followed:
- every CLI login token was a potential estate-administration token, bounded only by the user’s role;
- the only audience common to every break-glass token was the estate-wide
stellaops, which is why the router gateway’s introspection identity forfindings:recoveryhad to be registered under it (SPRINT_20260722_010FND-17).
This page records the census that decided what the CLI client keeps, where everything else went, and the two mechanisms that keep the split from silently regressing.
Method — how “the CLI needs it” was decided
A scope stays on stellaops-cli when one of three machine-checkable facts holds:
| Tier | Evidence | Count |
|---|---|---|
| 1 | src/Cli (non-test) references the StellaOpsScopes constant or the exact string literal | 41 |
| 2 | the CLI’s own product documentation (docs/modules/cli/**) names the scope | +19 |
| 3 | a route literal in src/Cli targets an endpoint whose authorization policy requires the scope | +29 |
| OIDC | openid profile email offline_access | +4 |
orch:quota is named in docs/modules/cli but excluded: the token endpoint requires quota_reason for it and the CLI never sends one (S001 records the same decision). Tier 1 alone is the “27 constants” figure often quoted; it under-counts because the CLI supplies most scopes through the operator’s configured Authority:Scope rather than naming them, and because a whole endpoint group can be gated by a scope the CLI never spells out (/console/* requires ui.read, so stella auth token mint needs it).
Everything else on the live row (59 scopes) leaves the CLI client. Each was checked for a consumer: operator scripts under devops/ and tools/, .gitea workflows, QA harnesses and fixtures under src/__Tests and src/Web, and runbook curl recipes. The consumers group into five operational procedures, and each procedure now has its own password-grant identity carrying exactly the union of scopes its scripts request.
Eight ceiling scopes are not granted today and were not added by this change (a narrowing change adds nothing): authority:idp.read, authority:idp.write, obs:read, ops.admin, platform.setup.admin, platform.setup.read, policy:promote, vex:ingest. The CLI names them; the corresponding verbs cannot mint through stellaops-cli until an operator adds them, which the ceiling permits.
Table A — the ceiling (93 scopes; 85 granted)
2026-09-14 (SPRINT_20260914_002 EA-5): envstate:time-anchor:write joined the ceiling and the grant. stella airgap time-anchor declare PUTs /api/v1/platform/environment-state/time-anchor/{scope} and Platform’s custody policy for that class accepts only this scope (no legacy alias, unlike airgap-seal); the CLI now requests it per class (BackendOperationsClient.ResolveEnvironmentStateWriteScope). The widening shipped as migration S060 in S058’s intersection shape — S058 is applied and checksum-pinned, so it is never edited; the guard asserts S058 ⊂ S060.
| Scope | Tier | Granted to stellaops-cli | Evidence |
|---|---|---|---|
advisory-ai:operate | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Program.cs:194 |
advisory-ai:view | 3 (CLI route to endpoint policy) | yes | CLI advisory-ai chat/rate-limit GETs (dominated by advisory-ai:operate, kept for least-privilege reads) -> AdvisoryAI.WebService/Security/AdvisoryAIPolicies.cs:55 (ViewPolicy) |
advisory:ingest | 2 (CLI docs) | yes | docs/modules/cli/guides/commands/auth.md:56 |
advisory:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Program.cs:829 |
airgap:import | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:2214 |
airgap:seal | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:133 |
airgap:status:read | 3 (CLI route to endpoint policy) | yes | CLI airgap status (/api/offlinekit/v1) -> OfflineKit.WebService/Program.cs (OfflineKitPolicies.StatusRead) |
analytics.read | 2 (CLI docs) | yes | docs/modules/cli/guides/commands/analytics.md:6 |
aoc:verify | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Program.cs:830 |
attest:create | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/AttestorLinkClient.cs:62 |
attest:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/AttestorLinkClient.cs:63 |
authority:idp.read | 1 (CLI source) | no (pre-existing gap, not added) | src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:3132 |
authority:idp.write | 1 (CLI source) | no (pre-existing gap, not added) | src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:3139 |
authority:roles.read | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:6541 (GET console/admin/roles) -> Authority/Console/Admin/ConsoleAdminEndpointExtensions.cs:161 |
authority:roles.write | 3 (CLI route to endpoint policy) | yes | BackendOperationsClient.cs:6564,6594 (POST/PATCH console/admin/roles) -> ConsoleAdminEndpointExtensions.cs:166,173 |
authority:tenants.read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Commands/Nis2SoaCommandGroup.cs:300 |
authority:tenants.write | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Commands/Assurance/OperatorComplianceConfigCommandGroup.cs:338 (PUT operator-compliance) -> ConsoleAdminEndpointExtensions.cs:88-89 |
concelier.jobs.trigger | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Configuration/AuthorityTokenUtilities.cs:25 |
concelier.merge | 2 (CLI docs) | yes | docs/modules/cli/architecture.md:210 |
crypto:admin | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/PlatformSecretProviderClient.cs:127 |
crypto:kek:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/PlatformCryptoKekClient.cs:71 |
crypto:kek:rotate | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/PlatformCryptoKekClient.cs:72 |
crypto:profile:admin | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/TenantCryptoComplianceClient.cs:41 |
crypto:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/PlatformCryptoKekClient.cs:73 |
email | OIDC | yes | standard OpenID Connect scope |
envstate:time-anchor:write | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs (ResolveEnvironmentStateWriteScope, PUT api/v1/platform/environment-state/time-anchor/{scope}) -> Platform.WebService/Program.cs (PlatformPolicies.EnvironmentStateTimeAnchorWrite) |
evidence:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/ForensicSnapshotClient.cs:344 |
exceptions:approve | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Services/ExceptionClient.cs (/{id}/{transition}) -> Policy.Engine/Endpoints/Gateway/ExceptionApprovalEndpoints.cs:65-85 |
exceptions:read | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Services/ExceptionClient.cs (/api/policy/exceptions) -> Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs:41 |
exceptions:request | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Services/ExceptionClient.cs (POST /api/policy/exceptions) -> Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs:34 |
export.operator | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Commands/AssuranceProduceClient.cs:34 |
export.viewer | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Commands/AssuranceProduceClient.cs:34 |
findings:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/CvssClient.cs:69 |
findings:write | 2 (CLI docs) | yes | docs/modules/cli/findings-runtime.md:51 |
graph:read | 3 (CLI route to endpoint policy) | yes | CLI /api/graphs/ -> Graph.Api/Security/GraphPolicies.cs:36 |
integration:operate | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Commands/GuardCommandGroup.cs:175 |
integration:read | 3 (CLI route to endpoint policy) | yes | CLI /api/v1/integrations, /api/v1/sources, /api/v1/feeds -> Integrations.WebService/Program.cs (IntegrationPolicies.Read) |
integration:write | 3 (CLI route to endpoint policy) | yes | CLI /api/v1/integrations (register/update) -> Integrations.WebService/Program.cs (IntegrationPolicies.Write) |
notify.operator | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/Nis2IncidentClient.cs:25 |
notify.viewer | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/Nis2IncidentClient.cs:26 |
obs:read | 1 (CLI source) | no (pre-existing gap, not added) | src/Cli/StellaOps.Cli/Services/ObservabilityClient.cs:177 |
offline_access | OIDC | yes | standard OpenID Connect scope |
openid | OIDC | yes | standard OpenID Connect scope |
ops.admin | 2 (CLI docs) | no (pre-existing gap, not added) | docs/modules/cli/guides/admin/admin-reference.md:35 |
ops.health | 2 (CLI docs) | yes | docs/modules/cli/guides/admin/admin-reference.md:35 |
orch:backfill | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Configuration/AuthorityTokenUtilities.cs:62 |
orch:operate | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Configuration/AuthorityTokenUtilities.cs:55 |
orch:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/JobEngineClient.cs:394 |
packs.approve | 2 (CLI docs) | yes | docs/modules/cli/guides/packs-profiles.md:47 |
packs.read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/PackClient.cs:74 |
packs.run | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/PackClient.cs:136 |
packs.write | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/PackClient.cs:314 |
platform.setup.admin | 1 (CLI source) | no (pre-existing gap, not added) | src/Cli/StellaOps.Cli/Services/PlatformEnvironmentSettingsClient.cs:30 |
platform.setup.read | 1 (CLI source) | no (pre-existing gap, not added) | src/Cli/StellaOps.Cli/Services/PlatformEnvironmentSettingsClient.cs:29 |
policy:activate | 2 (CLI docs) | yes | docs/modules/cli/guides/policy.md:6 |
policy:audit | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Commands/Nis2SoaCommandGroup.cs:293 (api/v1/tenants/{t}/compliance-profile) -> Authority/Tenants/TenantComplianceProfileEndpointExtensions.cs:37 |
policy:edit | 2 (CLI docs) | yes | docs/modules/cli/guides/policy.md:6 |
policy:promote | 2 (CLI docs) | no (pre-existing gap, not added) | docs/modules/cli/guides/cli-reference.md:495 |
policy:publish | 2 (CLI docs) | yes | docs/modules/cli/guides/cli-reference.md:495 |
policy:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/ExceptionClient.cs:70 |
policy:run | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/CvssClient.cs:57 |
policy:simulate | 3 (CLI route to endpoint policy) | yes | CLI policy simulate (/api/v2/simulate, /api/vuln/simulate, /api/risk/simulate) -> Policy.Engine/Endpoints/ConsoleSimulationEndpoint.cs:19; JobEngine PolicySimulationEndpointExtensions.cs:20 |
profile | OIDC | yes | standard OpenID Connect scope |
registry.admin | 3 (CLI route to endpoint policy) | yes | RegistryCommandGroup.cs:1158,1204 (PUT/DELETE /api/v1/registry/upstreams) -> ReleaseOrchestratorPolicies.cs (RegistryUpstreamAdmin) |
registry:cache:admin | 3 (CLI route to endpoint policy) | yes | RegistryCommandGroup.cs:974,1022 (PUT/DELETE /api/v1/registry/cache/policies) -> ReleaseOrchestratorPolicies.cs (RegistryCacheAdmin) |
registry:read | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Commands/RegistryCommandGroup.cs (GET upstreams, cache/stats) -> ReleaseOrchestratorPolicies.cs (RegistryUpstreamRead/RegistryCacheRead) |
release:publish | 3 (CLI route to endpoint policy) | yes | CLI /api/v1/release-orchestrator/approvals -> ReleaseOrchestratorPolicies.cs:100 (ReleaseApprove = ReleasePublish) |
release:read | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/OperatorDecisionHttpClientRegistration.cs (/api/v1/release-orchestrator/*) -> ReleaseOrchestrator.WebApi/ReleaseOrchestratorPolicies.cs (ReleaseRead) |
release:write | 3 (CLI route to endpoint policy) | yes | CLI release approve/reject/promote/deploy/rollback (/api/v1/release-orchestrator/operator-decisions) -> ReleaseOrchestratorPolicies.cs:94 (ReleaseWrite) |
replay:read | 3 (CLI route to endpoint policy) | yes | CLI /api/v1/replay/bundles (GET only) -> Replay.WebService/ReplayAuthorizationPolicies.cs:41 |
sbom:read | 3 (CLI route to endpoint policy) | yes | CLI /api/v1/sboms, /api/v1/sboms/{id}/export -> SbomService/Program.cs:98 (SbomPolicies.Read) |
sbom:write | 3 (CLI route to endpoint policy) | yes | CLI /api/v1/sbom/upload -> SbomService/Program.cs:1518-1520 (SbomPolicies.Write) |
scanner:export | 2 (CLI docs) | yes | docs/modules/cli/architecture.md:719 |
scanner:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/ScmImportClient.cs:93 |
scanner:scan | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/ScmImportClient.cs:69 |
scanner:write | 3 (CLI route to endpoint policy) | yes | CLI /api/v1/scans, /api/v1/triage, /api/v1/sources -> Scanner.WebService/Program.cs (ScansWrite/SourcesWrite/TriageWrite) |
scheduler:read | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Services/JobEngineClient.cs:404 (/api/v1/jobengine) -> JobEngine.Scheduling/JobEngineJobs/JobEngineJobEndpointExtensions.cs:29 (SchedulerPolicies.JobEngineRead) |
score.evaluate | 3 (CLI route to endpoint policy) | yes | CLI api/v1/score/evaluate -> Signals/Score/Endpoints/ScoreEndpoints.cs:92 (ScorePolicies.ScoreEvaluate) |
score.read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Program.cs:803 |
signer:read | 2 (CLI docs) | yes | docs/modules/cli/architecture.md:522 |
signer:sign | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Commands/DoraRoiSigningPipelineFactory.cs:196 |
timeline:read | 3 (CLI route to endpoint policy) | yes | CLI /api/v1/timeline/events, /snapshots (GET only) -> Timeline.WebService/Program.cs (TimelinePolicies.Read) |
trust:read | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Commands/WatchlistCommandGroup.cs (GET /api/v1/watchlist) -> Attestor.Application/AttestorWebServiceComposition.cs:390 (watchlist:read) |
trust:write | 3 (CLI route to endpoint policy) | yes | WatchlistCommandGroup.cs (POST/PUT/DELETE /api/v1/watchlist) -> AttestorWebServiceComposition.cs:405 (watchlist:write) |
ui.admin | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/BackendOperationsClient.cs:3131 |
ui.read | 3 (CLI route to endpoint policy) | yes | src/Cli/StellaOps.Cli/Services/AuthorityConsoleClient.cs (console/tenants, console/token/*) -> StellaOps.Authority/Console/ConsoleEndpointExtensions.cs:27 (/console group RequireStellaOpsScopes(UiRead)) |
vex.admin | 2 (CLI docs) | yes | docs/modules/cli/architecture.md:722 |
vex:ingest | 2 (CLI docs) | no (pre-existing gap, not added) | docs/modules/cli/architecture.md:722 |
vex:read | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/VexObservationsClient.cs:146 |
vuln:audit | 2 (CLI docs) | yes | docs/modules/cli/guides/vuln-explorer-cli.md:19 |
vuln:investigate | 2 (CLI docs) | yes | docs/modules/cli/guides/vuln-explorer-cli.md:17 |
vuln:operate | 2 (CLI docs) | yes | docs/modules/cli/guides/vuln-explorer-cli.md:18 |
vuln:view | 1 (CLI source) | yes | src/Cli/StellaOps.Cli/Services/ConcelierObservationsClient.cs:364 |
Table B — the 59 scopes that left stellaops-cli
“Consumers” counts the census entries (Table C) that requested the scope through stellaops-cli.
| Scope | Consumers | New home | Note |
|---|---|---|---|
authority.audit.read | 0 | none; per incident on stellaops-break-glass-operator | console audit read; no CLI route |
authority.revocation.read | 0 | none; per incident on stellaops-break-glass-operator | /revocation/status read; the CLI reads revocation exports through the bootstrap key, not this scope |
authority:clients.read | 2 | stellaops-estate-admin-operator, stellaops-onboarding-operator | console client administration; no CLI route |
authority:clients.write | 2 | stellaops-estate-admin-operator, stellaops-onboarding-operator | console client administration; no CLI route |
authority:signing-keys.admin | 0 | none; per incident on stellaops-break-glass-operator | operator signing administration; no CLI verb |
authority:signing-keys.enroll | 0 | none; per incident on stellaops-break-glass-operator | operator signing enrollment; no CLI verb |
authority:tokens.read | 0 | stellaops-estate-admin-operator | console token inventory; no CLI route |
authority:tokens.revoke | 0 | stellaops-estate-admin-operator | POST /console/admin/tokens/revoke, the break-glass revoke step |
authority:users.read | 4 | stellaops-estate-admin-operator, stellaops-onboarding-operator | console user administration; no CLI route |
authority:users.write | 4 | stellaops-estate-admin-operator, stellaops-onboarding-operator | console user administration; no CLI route |
decision-signing | 0 | none; per incident on stellaops-break-glass-operator | no CLI verb |
doctor:admin | 0 | none; per incident on stellaops-break-glass-operator | no endpoint requires it (S049 retired the doctor scopes) |
doctor:run | 0 | none; per incident on stellaops-break-glass-operator | no endpoint requires it (S049 retired the doctor scopes) |
evidence:create | 5 | stellaops-onboarding-operator, stellaops-compliance-operator, stellaops-release-ops-operator | the CLI only reads evidence; compliance, onboarding and release-ops windows create it |
evidence:hold | 1 | stellaops-release-ops-operator | evidence legal hold (VerdictPolicies.Admin); release-ops window |
export.admin | 0 | none; per incident on stellaops-break-glass-operator | profile archive / legacy DELETE; no CLI verb |
issuer-directory:admin | 0 | none; per incident on stellaops-break-glass-operator | see issuer-directory:read |
issuer-directory:read | 0 | none; per incident on stellaops-break-glass-operator | the CLI states no issuer-directory backend is wired |
issuer-directory:write | 0 | none; per incident on stellaops-break-glass-operator | see issuer-directory:read |
notify.admin | 0 | none; per incident on stellaops-break-glass-operator | channel create/test/send/retry are notify.operator; no CLI route needs admin |
notify.escalate | 0 | none; per incident on stellaops-break-glass-operator | no CLI verb |
operator.verify.admin | 0 | none; per incident on stellaops-break-glass-operator | no policy registration or endpoint requires it |
operator.verify.read | 0 | none; per incident on stellaops-break-glass-operator | no policy registration or endpoint requires it |
orch:quota | 0 | none; per incident on stellaops-break-glass-operator | client_credentials quota-raise scope; the CLI never sends quota_reason (S001 decision) |
platform.context.read | 5 | stellaops-onboarding-operator | /api/v2/context/* (Platform); the CLI never calls it. QA harnesses requested it without using it |
platform.context.write | 2 | stellaops-onboarding-operator | /api/v2/context/* (Platform); the CLI never calls it |
platform:federation:read | 0 | none; per incident on stellaops-break-glass-operator | ReleaseOrchestrator federation; the CLI federation verbs target Concelier (Concelier.Advisories.*) |
platform:federation:write | 0 | none; per incident on stellaops-break-glass-operator | see platform:federation:read |
platform:sar:erase | 1 | stellaops-estate-admin-operator | subject-access erasure (privacy administration) |
platform:sar:read | 1 | stellaops-estate-admin-operator | subject-access requests (privacy administration) |
policy:approve | 0 | none; per incident on stellaops-break-glass-operator | promotion-gate override; not a CLI route |
policy:author | 5 | stellaops-compliance-operator | exception/gate authoring endpoints the CLI does not call; the compliance golden path uses it |
policy:operate | 0 | none; per incident on stellaops-break-glass-operator | budget check / console export jobs; the CLI only lists budgets (policy:read) |
policy:review | 0 | none; per incident on stellaops-break-glass-operator | no serving endpoint; the CLI review verbs target routes no backend registers |
registry.token.issue | 0 | none; per incident on stellaops-break-glass-operator | registry token service (audience registry); no CLI verb |
release.policy-bindings.read | 2 | stellaops-release-ops-operator | environment policy bindings; Invoke-Ef5SameSubjectProof.ps1 PUTs them |
release.policy-bindings.write | 2 | stellaops-release-ops-operator | see release.policy-bindings.read |
release.policy.manage | 0 | none; per incident on stellaops-break-glass-operator | approval-policy routes; no CLI verb |
release.policy.read | 0 | none; per incident on stellaops-break-glass-operator | approval-policy routes; no CLI verb |
replay:write | 0 | none; per incident on stellaops-break-glass-operator | the CLI only downloads replay bundles |
router:routing:read | 0 | none; per incident on stellaops-break-glass-operator | no policy registration or endpoint requires it |
router:routing:write | 0 | none; per incident on stellaops-break-glass-operator | no policy registration or endpoint requires it |
sbom:attest | 0 | none; per incident on stellaops-break-glass-operator | POST /api/v1/lineage/diff/export; the CLI export lineage verb is local |
sbom:operate | 0 | none; per incident on stellaops-break-glass-operator | SbomPolicies.Internal (service-to-service) |
scheduler:operate | 1 | stellaops-onboarding-operator | JobEngine run operations; the CLI JobEngine client only needs scheduler:read (JobEngineRead group) |
signals:read | 0 | none; per incident on stellaops-break-glass-operator | DB-only survivor (S001), no consumer |
signals:write | 0 | none; per incident on stellaops-break-glass-operator | DB-only survivor (S001), no consumer |
signer:admin | 0 | none; per incident on stellaops-break-glass-operator | Signer ceremony approver; no CLI verb |
signer:rotate | 0 | none; per incident on stellaops-break-glass-operator | Signer key management; no CLI verb |
stellaops.admin | 0 | none; per incident on stellaops-break-glass-operator | not in the scope catalogue; S001 seed survivor |
stellaops.api | 3 | none; per incident on stellaops-break-glass-operator | not in the scope catalogue; already refused invalid_scope before AUTH-15 |
timeline:admin | 0 | none; per incident on stellaops-break-glass-operator | no consumer |
timeline:write | 0 | none; per incident on stellaops-break-glass-operator | the CLI only reads the timeline |
trust:admin | 0 | none; per incident on stellaops-break-glass-operator | no watchlist route requires it |
ui.preferences.read | 0 | none; per incident on stellaops-break-glass-operator | Console-only |
ui.preferences.write | 0 | none; per incident on stellaops-break-glass-operator | Console-only |
vexhub:read | 1 | none; per incident on stellaops-break-glass-operator | no endpoint enforces it; dropped from the stella-connectors.sh password default |
vexlens.read | 2 | none; per incident on stellaops-break-glass-operator | no endpoint enforces it (/api/v1/vexlens is served by no service); two Web e2e specs still request it, see Limitations |
vexlens.write | 1 | none; per incident on stellaops-break-glass-operator | no endpoint enforces it; see vexlens.read |
Table C — every consumer that minted through stellaops-cli, and where it mints now
| Consumer | Procedure | Scopes requested | Client after AUTH-15 | Change |
|---|---|---|---|---|
.gitea/workflows/auto-sbom-readiness-e2e.yml | ci-e2e | 6 | stellaops-cli | unchanged (all kept) |
.gitea/workflows/console-layout-audit.yml | ci-e2e | 2 | stellaops-cli | unchanged (OIDC only) |
.gitea/workflows/e2e-analyzer-coverage.yml | ci-e2e | 6 | stellaops-cli | unchanged (all kept) |
devops/agents-targets/scripts/customer-onboarding/01b-register-inline-integrations.sh | onboarding | 9 | stellaops-onboarding-operator | unchanged |
devops/agents-targets/scripts/customer-onboarding/_common.ps1 | onboarding | 31 | stellaops-onboarding-operator | unchanged scope string |
devops/compose/docker-compose.cli.yml | cli-default | 4 | stellaops-cli | default STELLAOPS_AUTHORITY_SCOPE loses stellaops.api (already refused) |
devops/compose/env/stella-cli.env.example | cli-default | 4 | stellaops-cli | default STELLAOPS_AUTHORITY_SCOPE loses stellaops.api (already refused) |
devops/compose/scripts/compliance-golden-path.ps1 | compliance | 18 | stellaops-compliance-operator | default $ClientId; unchanged scopes |
devops/compose/scripts/compliance-golden-path.sh | compliance | 19 | stellaops-compliance-operator | default CLIENT_ID; unchanged scopes |
devops/compose/scripts/compliance-setup.ps1 | compliance | 5 | stellaops-compliance-operator | default $ClientId; unchanged scopes |
devops/compose/scripts/compliance-setup.sh | compliance | 5 | stellaops-compliance-operator | default CLIENT_ID; unchanged scopes |
devops/compose/scripts/sprint005-cycle003-verify.sh | probe | 3 | stellaops-cli | unchanged (OIDC only) |
devops/compose/scripts/stella-connectors.sh | connectors-password-path | 11 | stellaops-cli (opt-in password path; default isstellaops-cli-automation) | vexhub:read dropped from OIDC_SCOPE_PASSWORD |
docs/runbooks/general/tenant-onboarding.md | runbook | 6 | step 1stellaops-estate-admin-operator; step 4stellaops-cli`` | unchanged scopes |
src/Web/StellaOps.Web/e2e/subject-access-request.e2e.spec.ts | qa-harness | 5 | stellaops-estate-admin-operator | unchanged scopes |
src/Web/StellaOps.Web/e2e/vexlens-token-scope-forcing-function.e2e.spec.ts | qa-harness | 3 | stellaops-cli (unchanged) | requests vexlens.read, which no client can mint; see Limitations |
src/Web/StellaOps.Web/tests/e2e/integrations/distro-vex-advisory-only-not-capped.spec.ts | qa-harness | 6 | stellaops-cli (unchanged) | requests vexlens.read/write and posts to /api/v1/vexlens/*, which no service serves; see Limitations |
src/Web/StellaOps.Web/tests/e2e/integrations/empty-topology-live.spec.ts | qa-harness | 8 | stellaops-estate-admin-operator | unchanged scopes |
src/__Tests/Fixtures/Setup-TwoTenantFixture.ps1 | qa-harness | 7 | stellaops-estate-admin-operator | unchanged scopes |
src/__Tests/playwright/_shared/auth.fixture.ts | qa-harness | 3 | stellaops-cli | default scope openid profile stellaops.api -> openid profile (the old default was already refused) |
tools/scripts/deploy/customer/scan-reachability.ps1 | qa-scan | 8 | stellaops-cli | unchanged (all kept) |
tools/scripts/lab/seed-packsregistry-lab-data.sh | lab-seed | 4 | stellaops-cli | unchanged (all kept) |
tools/scripts/qa/Capture-Baseline.ps1 | qa-scan | 17 | stellaops-cli | platform.context.read dropped (requested, never used) |
tools/scripts/qa/Invoke-AdvancedAssuranceRerun.ps1 | probe | 3 | stellaops-cli | unchanged; requests roles, which is not a scope (pre-existing, see Limitations) |
tools/scripts/qa/Invoke-Ef5SameSubjectProof.ps1 | qa-release | 17 | stellaops-release-ops-operator | unchanged scopes (PUTs environment policy bindings) |
tools/scripts/qa/Invoke-OperatorGoldenRerun.ps1 | qa-release | 20 | stellaops-cli | evidence:create evidence:hold dropped (requested, never used) |
tools/scripts/qa/Invoke-OperatorScenarios.ps1 | qa-scan | 17 | stellaops-cli | platform.context.read dropped (requested, never used) |
tools/scripts/qa/Invoke-SowLiveCaptures.ps1 | estate-admin | 5 | stellaops-estate-admin-operator | unchanged scopes |
tools/scripts/qa/Invoke-Wave2IsolateRetest.ps1 | estate-admin | 5 | stellaops-estate-admin-operator | unchanged scopes |
tools/scripts/qa/Provision-Ef5SourceFixture.ps1 | qa-release | 28 | stellaops-cli | release.policy-bindings.read/write dropped (requested, never used) |
tools/scripts/qa/rar5-route-reachability-probe.py | compliance | 19 | stellaops-compliance-operator | default CLIENT_ID; unchanged scopes |
tools/scripts/qa/seed-real-scan.ps1 | qa-scan | 12 | stellaops-cli | unchanged (all kept) |
tools/scripts/qa/verify-os-analyzer-scan.ps1 | qa-scan | 7 | stellaops-cli | unchanged (all kept) |
tools/scripts/qa/verify-rar7-native-route-window.mjs | probe | 4 | stellaops-cli | unchanged (all kept) |
tools/scripts/run-console-four-role-live.ps1 | estate-admin | 5 | stellaops-estate-admin-operator | unchanged scopes |
Consumers that stay on stellaops-cli do so because every scope they request is a product-CLI scope: they simulate a CLI user and should keep exercising the product path. Where a harness requested a scope it never used (platform.context.read, release.policy-bindings.*, evidence:create/hold), the request was trimmed rather than the identity widened.
Table D — the operator identities
All five follow the shape stellaops-advisory-ai-operator established: allowedGrantTypes: password (human-bound, role-gated), tenantId and tenants pinned to default (two tenants without a singular tenantId suppresses the tenant bind and advisory scopes are rejected), allowedAudiences: stellaops only (the router gateway validates aud=stellaops; per-service audiences are inert under Router__AuthorizationTrustMode=GatewayEnforced), public (the credential is the operator’s own login).
| Client | Scopes | Grant |
|---|---|---|
stellaops-estate-admin-operator | 20 | authority:clients.read authority:clients.write authority:roles.read authority:roles.write authority:tenants.read authority:tenants.write authority:tokens.read authority:tokens.revoke authority:users.read authority:users.write email evidence:read integration:read offline_access openid platform:sar:erase platform:sar:read profile ui.admin ui.read |
stellaops-onboarding-operator | 32 | authority:clients.read authority:clients.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write evidence:create evidence:read findings:read findings:write graph:read integration:operate integration:read integration:write offline_access openid orch:backfill orch:operate orch:read platform.context.read platform.context.write profile registry.admin release:publish release:read release:write sbom:read scanner:read scanner:write scheduler:operate scheduler:read ui.admin |
stellaops-compliance-operator | 22 | authority:tenants.read authority:tenants.write evidence:create evidence:read export.operator export.viewer notify.operator notify.viewer offline_access openid orch:operate orch:read policy:author policy:read profile release:read release:write sbom:read sbom:write scanner:read scanner:scan ui.admin |
stellaops-release-ops-operator | 23 | attest:read email evidence:create evidence:hold evidence:read export.operator export.viewer findings:read offline_access openid orch:operate orch:read policy:read profile release.policy-bindings.read release.policy-bindings.write release:publish release:read release:write replay:read scanner:read scanner:write vuln:operate |
stellaops-break-glass-operator | 2 | openid profile |
Which identity an operator reaches for:
| Procedure | Identity |
|---|---|
| tenant / user / client / role / token administration over the Authority console API; subject-access requests; console role harnesses | stellaops-estate-admin-operator |
customer onboarding (devops/agents-targets/scripts/customer-onboarding) | stellaops-onboarding-operator |
| compliance golden path, operator-compliance setup, route-reachability probes | stellaops-compliance-operator |
| release-orchestration windows: environment policy bindings, evidence hold | stellaops-release-ops-operator |
| AdvisoryAI / OpsMemory forcing functions | stellaops-advisory-ai-operator (unchanged) |
findings:recovery, and any scope no standing identity carries, appended for the duration of an incident and reset after | stellaops-break-glass-operator |
| anything a CLI verb does | stellaops-cli |
The stella CLI itself can mint through any of them: set STELLAOPS_AUTHORITY_CLIENT_ID (or --Authority:ClientId) to the identity and request only the scopes the procedure needs.
Scope break-glass after this change
This is the scope break-glass (an elevated grant for one incident). It is distinct from the credential-recovery break-glass in docs/runbooks/authority/authority-break-glass-recovery.md.
- Scope issuance is gated by the client ceiling only.
PasswordGrantHandlersresolves the requested scopes against the client’sallowedScopes(ClientCredentialsHandlers.ResolveGrantedScopes); there is no intersection with the user’s role permissions at mint time. Consequently a scope that sits on any public password-grant client is mintable by every user who can log in — which is exactly why the CLI client’s ceiling is a security boundary and whystellaops-break-glass-operatorcarries no break-glass scope at rest (openid profile; non-empty because an empty allowed set grants anything requested). findings:recoveryis granted to no bootstrap client (RepositoryStandardDescriptors_GrantFindingsRecoveryToNoClientpins the FND-17 doctrine). The procedure indocs/runbooks/findings/findings-service-deploy.md: append the scope to the break-glass row for the incident, assign theincident-findings-recoveryrole, mint throughstellaops-break-glass-operator, run the audited transitions, revoke role and token, then reset the break-glass row in the database (the reseed never removes what was appended).- Any other emergency scope (the “none” rows of Table B:
signer:rotate,authority:signing-keys.*,issuer-directory:admin,platform:federation:*, …) follows the same path — onto the break-glass row for the incident, through the Console client editor (authority:clients.write, i.e. viastellaops-estate-admin-operator) orstandard.yaml+ restart, and off it again with the sameUPDATE. Never ontostellaops-cli: the ceiling test fails the commit and the migration removes it from the database. - Superseded 2026-09-08 by AUTH-17. When this census was written the gateway’s break-glass introspection client was registered under
stellaops— the audience every token in the estate carries — so it covered every break-glass token by construction, whichever client minted it. AUTH-17 narrowed it: the introspection client is nowstellaops-gateway-introspection, onlystellaops-break-glass-operatornames that id in itsallowedAudiences, and the estate-widestellaopsclient is deleted (migrationS059_retire_estate_wide_introspection_client.sql). Consequence for the procedures above: afindings:recoverytoken must be minted throughstellaops-break-glass-operator, or the gateway refuses it503 break_glass_introspection_not_authorized_partybefore any revocation question is asked.
The union-reseed asymmetry — why the database step is mandatory
StandardClientProvisioningStore.CreateOrUpdateAsync merges DB ∪ YAML for allowedScopes, audiences and tenants on every Authority boot and never shrinks a grant (Sprint 20260520_084, RESIDUAL-2). Therefore:
- Adding a scope to
standard.yamllands on the live row with a plain restart. - Removing a scope from
standard.yamlchanges nothing on a running estate, and on a fresh databaseS001+S034…S057re-append the historical grants before the bootstrapper runs.
The narrowing is therefore a forward-only migration, S058_stellaops_cli_scope_ceiling.sql, which sorts after every append migration and intersects the persisted row with the ceiling. It converges an existing estate (on the first boot of an image that embeds it) and a fresh bootstrap (after S057) on the same set. Because embedded SQL needs an image rebuild, an estate that has not taken that image applies the identical statement by hand:
bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --apply # snapshots the row first
bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --verify # row ⊆ ceiling, reads the DB
bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --rollback # the 143-scope pre-change row
bash tools/scripts/deploy/postgres/apply-stellaops-cli-scope-ceiling.sh --self-test # proves --verify detects
The script parses the ceiling out of the migration file rather than carrying its own copy, so the two cannot disagree. The rollback file (rollback-stellaops-cli-scope-ceiling.sql) is the exact pre-change row captured on 2026-09-05 — 139 YAML scopes plus four DB-only survivors (signals:read, signals:write, stellaops.admin, stellaops.api) — and RollbackScript_RestoresThePreChangeRowAndCoversEveryCurrentGrant pins that it is a superset of the current grant. Restoring the YAML alone is not a rollback: the reseed re-adds exactly what the YAML declares and nothing else.
Verify against the database, never the file. The YAML is the intent; the row is what Authority mints from.
The guard
StellaOpsCliClientScopeCeilingTests (StellaOps.Authority.Plugin.Standard.Tests) pins:
- the ceiling is catalogued, distinct, sorted and non-vacuous (93 entries, every one in
StellaOpsScopes.Allor OIDC, the 28 source constants present); stellaops-cli’sallowedScopesinstandard.yaml⊆ ceiling, and carriesopenid profile email offline_accessand at least 50 scopes (a rewrite that drops half the grant fails too);- the
ARRAY[...]literal in the CURRENT ceiling migration (S060) equals the ceiling, and the prior one (S058) is a strict subset of it — a successor never drops a scope; - the rollback file lists the 143-scope pre-change row plus the EA-5 scope (144) and is a superset of the current grant;
- the five operator identities have the D-AAI9 shape and only catalogued scopes; the break-glass identity is exactly
openid profile; findings:recoveryis granted to no bootstrap client at all.
Red-proof (2026-09-05): adding signer:rotate to the YAML grant fails fact 2 naming signer:rotate; deleting vuln:operate from the migration array fails fact 3 naming it. The deploy script’s --self-test proves its own detector the same way (an in-ceiling row passes, one out-of-ceiling scope fails).
Limitations recorded, not fixed
src/Web/StellaOps.Web/e2e/vexlens-token-scope-forcing-function.e2e.spec.tsandsrc/Web/StellaOps.Web/tests/e2e/integrations/distro-vex-advisory-only-not-capped.spec.tsmintvexlens.read/vexlens.writethroughstellaops-cli. No endpoint enforces either scope and no service registers/api/v1/vexlens/*, so both specs were already non-functional against the current estate; they now also fail at the token endpoint (invalid_scope). They need re-homing onto thestella-ops-uigrant they actually assert, which is Console work, not Authority work.tools/scripts/qa/Invoke-AdvancedAssuranceRerun.ps1requestsroles, which is not a scope; the request was refused before this change and is unaffected by it.stellaops-clikeeps audiencessignerandregistry(needed by the keptsigner:signandregistry.*scopes) andapi://issuer-directory(no kept scope needs it; audiences union on reseed exactly like scopes, so removing it is its own DB step and was not part of this change).- The eight ceiling scopes the CLI names but is not granted (see Method) remain a pre-existing gap.
stellaops-clistill declarestenants: default tenant-golden-local; the operator identities aredefault-only and gain further tenants only through the console tenant-create auto-grant, which extends everyfirstPartyclient.
