StellaOps Authority Service

Status: Drafted 2025-10-12 (CORE5B.DOC / DOC1.AUTH) – aligns with Authority revocation store, JWKS rotation, and bootstrap endpoints delivered in Sprint 1.

1. Purpose

The StellaOps Authority service issues OAuth2/OIDC tokens for every StellaOps module (Concelier, Backend, Agent, Zastava) and exposes the policy controls required in sovereign/offline environments. Authority is built as a minimal ASP.NET host that:

Authority is deployed alongside Concelier in air-gapped environments and never requires outbound internet access. All trusted metadata (OpenIddict discovery, JWKS, revocation bundles) is cacheable, signed, and reproducible.

2. Component Architecture

Authority is composed of five cooperating subsystems:

  1. Minimal API host – configures OpenIddict endpoints (/token, /authorize, /logout (OIDC end-session / RP-Initiated Logout, advertised as end_session_endpoint), /revoke, /jwks), publishes the OpenAPI contract at /.well-known/openapi, and enables structured logging/telemetry. Rate limiting hooks (AuthorityRateLimiter) wrap every request.
  2. Plugin host – loads StellaOps.Authority.Plugin.*.dll assemblies, applies capability metadata, and exposes password/client provisioning surfaces through dependency injection.
  3. PostgreSQL storage – persists tokens, revocations, bootstrap invites, and plugin state in deterministic tables indexed for offline sync (authority_tokens, authority_revocations, etc.).
  4. Cryptography layerStellaOps.Cryptography abstractions manage password hashing, signing keys, JWKS export, and detached JWS generation.
  5. Offline ops APIs – internal endpoints under /internal/* provide administrative flows (bootstrap users/clients, revocation export) guarded by API keys and deterministic audit events.

A high-level sequence for password logins:

Client -> /token (password grant)
  -> Rate limiter & audit hooks
  -> Plugin credential store (Argon2id verification)
  -> Token persistence (PostgreSQL authority_tokens)
  -> Response (access/refresh tokens + deterministic claims)

3. Token Lifecycle & Persistence

Authority persists every issued token in PostgreSQL so operators can audit or revoke without scanning distributed caches.

Console OIDC client

Configuration sample (etc/authority.yaml.sample) seeds the client with a confidential secret so Console can negotiate the code exchange on the backend while browsers execute the PKCE dance.

Policy Studio scopes & signing workflow

Advisory AI scopes & remote inference

Authority publishes the trio in OpenID discovery (stellaops_advisory_ai_scopes_supported) so clients can self-discover capability. Remote/cloud inference is disabled by default; set advisoryAi.remoteInference.enabled: true and provide an explicit allowedProfiles whitelist (for example cloud-openai) when an installation opts in. The requireTenantConsent toggle (default true) enforces per-tenant opt-in before remote profiles are invoked, mirroring regulatory expectations for sovereign or air-gapped deployments.

Console Authority endpoints

All endpoints demand DPoP-bound tokens and propagate structured audit events (authority.console.*). Gateways must forward the X-StellaOps-TenantId header derived from the access token; downstream services rely on the same value for isolation. Keep Console access tokens short-lived (default 15 minutes) and enforce the fresh-auth window for admin actions (ui.admin, authority:*, policy:activate, exceptions:approve).

Expectations for resource servers

Resource servers (Concelier WebService, Backend, Agent) must not assume in-memory caches are authoritative. They should:

Tenant propagation

Default service scopes

Client IDPurposeScopes grantedSender constraintTenant
concelier-ingestConcelier raw advisory ingestionadvisory:ingest, advisory:readdpoptenant-default
excitor-ingestExcititor raw VEX ingestionvex:ingest, vex:readdpoptenant-default
aoc-verifierAggregation-only contract verificationaoc:verify, advisory:read, vex:readdpoptenant-default
signed-sbom-producerScanner signed-SBOM material producerscanner.signed-sbom-material.writedpoptenant-default
cartographer-serviceGraph snapshot constructiongraph:write, graph:readdpoptenant-default
graph-apiGraph Explorer gateway/APIgraph:read, graph:export, graph:simulatedpoptenant-default
export-center-operatorExport Center operator automationexport.viewer, export.operatordpoptenant-default
export-center-adminExport Center administrative automationexport.viewer, export.operator, export.admindpoptenant-default
notify-serviceNotify WebService APInotify.viewer, notify.operatordpoptenant-default
notify-adminNotify administrative automationnotify.viewer, notify.operator, notify.admindpoptenant-default
vuln-explorer-uiVuln Explorer UI/APIvuln:view, vuln:investigate, vuln:operate, vuln:auditdpoptenant-default
signals-uploaderReachability sensor ingestionsignals:write, signals:read, aoc:verifydpoptenant-default

Secret hygiene (2025‑10‑27): The repository includes a convenience etc/authority.yaml for compose/helm smoke tests. Every entry’s secretFile points to etc/secrets/*.secret, which ship with *-change-me placeholders—replace them with strong values (and wire them through your vault/secret manager) before issuing tokens in CI, staging, or production.

For factory provisioning, issue sensors the SignalsUploader role template (signals:write, signals:read, aoc:verify). Authority rejects ingestion tokens that omit aoc:verify, preserving aggregation-only contract guarantees for reachability signals.

These registrations are provided as examples in etc/authority.yaml.sample. Clone them per tenant (for example concelier-tenant-a, concelier-tenant-b) so tokens remain tenant-scoped by construction.

Authority publishes Scanner scopes in OpenID discovery under stellaops_scanner_scopes_supported, including scanner.signed-sbom-material.write. Production Scanner producer jobs should request that scope through the signed-sbom-producer client and svc-signed-sbom-producer delegated service account, or an equivalent tenant-local client/role binding.

Policy attestation metadata

Graph Explorer introduces dedicated scopes: graph:write for Cartographer build jobs, graph:read for query/read operations, graph:export for long-running export downloads, and graph:simulate for what-if overlays. Assign only the scopes a client actually needs to preserve least privilege—UI-facing clients should typically request read/export access, while background services (Cartographer, Scheduler) require write privileges.

Policy activation dual-control

Least-privilege guidance for graph clients

Export Center scope guardrails

Notify scope guardrails

Attachment signing tokens
curl -u vuln-explorer-worker:s3cr3t \
  -H "Content-Type: application/json" \
  -d '{
        "attachmentId": "finding-7d9d/evidence-2",
        "ledgerHash": "sha256:4a5160...",
        "metadata": { "download": "supporting-log.zip" }
      }' \
  https://authority.example.com/vuln/attachments/tokens/issue
Ledger verification workflow
  1. Resolve the attachment’s ledger entry (finding_history, triage_actions) and note the recorded hash/signature.
  2. Verify the issued attachment token via /vuln/attachments/tokens/verify; the response echoes the canonical hash and expiry.
  3. When downloading artefacts from Vuln Explorer, recompute the hash locally and compare it to both the ledger entry and the verified token payload.
  4. Cross-check Authority audit events (vuln.attachment.token.*) to confirm who issued and consumed the token; Offline Kit mirrors include the same audit feed.
Vuln Explorer security checklist

4. Revocation Pipeline

Authority centralises revocation in authority_revocations with deterministic categories:

CategoryMeaningRequired fields
tokenSpecific OAuth token revoked early.revocationId (token id), tokenType, optional clientId, subjectId
subjectAll tokens for a subject disabled.revocationId (= subject id)
clientOAuth client registration revoked.revocationId (= client id)
keySigning/JWE key withdrawn.revocationId (= key id)

RevocationBundleBuilder flattens PostgreSQL records into canonical JSON, sorts entries by (category, revocationId, revokedAt), and signs exports using detached JWS (RFC 7797) with cosign-compatible headers.

Export surfaces (deterministic output, suitable for Offline Kit):

Consumer guidance:

  1. Mirror revocation-bundle.json* alongside Concelier exports. Offline agents fetch both over the existing update channel.
  2. Use bundle sequence and bundleId to detect replay or monotonicity regressions. Ignore bundles with older sequence numbers unless bundleId changes and issuedAt advances.
  3. Treat revokedReason taxonomy as machine-friendly codes (compromised, rotation, policy, lifecycle). Translating to human-readable logs is the consumer’s responsibility.

5. Signing Keys & JWKS Rotation

Authority signs revocation bundles and publishes JWKS entries via the new signing manager:

Rotation SOP (no downtime)

  1. Generate a new P-256 private key (PEM) on an offline workstation and place it where the Authority host can read it (e.g., ../certificates/authority-signing-2025.pem).
  2. Call the authenticated admin API:
    curl -sS -X POST https://authority.example.com/internal/signing/rotate \
      -H "x-stellaops-bootstrap-key: ${BOOTSTRAP_KEY}" \
      -H "Content-Type: application/json" \
      -d '{
            "keyId": "authority-signing-2025",
            "location": "../certificates/authority-signing-2025.pem",
            "source": "file"
          }'
    
  3. Verify the response reports the previous key as retired and fetch /jwks to confirm the new kid appears with status: "active".
  4. Persist the old key path in signing.additionalKeys (the rotation API updates in-memory options; rewrite the YAML to match so restarts remain consistent).
  5. If you prefer automation, trigger the .gitea/workflows/authority-key-rotation.yml workflow with the new keyId/keyPath; it wraps ops/authority/key-rotation.sh and reads environment-specific secrets. The older key will be marked retired and appended to signing.additionalKeys.
  6. Re-run stella auth revoke export so revocation bundles are signed with the new key. Downstream caches should refresh JWKS within their configured lifetime (StellaOpsAuthorityOptions.Signing + client cache tolerance).

The rotation API leverages the same cryptography abstractions as revocation signing; no restart is required and the previous key is marked retired but kept available for verification.

6. Bootstrap & Administrative Endpoints

Administrative APIs live under /internal/* and require the bootstrap API key plus rate-limiter compliance.

EndpointMethodDescription
/internal/usersPOSTProvision initial administrative accounts through the registered password-capable plug-in. Emits structured audit events.
/internal/clientsPOSTProvision OAuth clients (client credentials / device code).
/internal/revocations/exportGETExport revocation bundle + detached JWS + digest.
/internal/signing/rotatePOSTPromote a new signing key (see SOP above). Request body accepts keyId, location, optional source, algorithm, provider, and metadata.

All administrative calls emit AuthEventRecord entries enriched with correlation IDs, PII tags, and network metadata for offline SOC ingestion.

Tenant hint: include a tenant entry inside properties when bootstrapping clients. Authority normalises the value, stores it on the registration, and stamps future tokens/audit events with the tenant.

Bootstrap client example

POST /internal/clients
{
  "clientId": "concelier",
  "confidential": true,
  "displayName": "Concelier Backend",
  "allowedGrantTypes": ["client_credentials"],
  "allowedScopes": ["concelier.jobs.trigger", "advisory:ingest", "advisory:read"],
  "properties": {
    "tenant": "tenant-default"
  }
}

For environments with multiple tenants, repeat the call per tenant-specific client (e.g. concelier-tenant-a, concelier-tenant-b) or append suffixes to the client identifier.

Aggregation-only verification tokens

Exception approvals & routing

exceptions:
  routingTemplates:
    - id: "secops"
      authorityRouteId: "approvals/secops"
      requireMfa: true
      description: "Security Operations approval chain"
    - id: "governance"
      authorityRouteId: "approvals/governance"
      requireMfa: false
      description: "Non-production waiver review"

Sealed-mode CI confirmation

Set airGap.sealedMode.enforcementEnabled: true to require sealed-mode evidence before issuing tokens to sensitive clients. The guard expects the DevOps harness (ops/devops/sealed-mode-ci/run-sealed-ci.sh) to upload authority-sealed-ci.json under artifacts/sealed-mode-ci/<timestamp>/. Configure Authority with the absolute or relative path to that file plus freshness/health requirements:

airGap:
  sealedMode:
    enforcementEnabled: true
    evidencePath: "artifacts/sealed-mode-ci/latest/authority-sealed-ci.json"
    maxEvidenceAge: "06:00:00"
    cacheLifetime: "00:01:00"
    requireAuthorityHealthPass: true
    requireSignerHealthPass: true
    requireAttestorHealthPass: true
    requireEgressProbePass: true

Only clients that set properties.requiresAirgapSealConfirmation: true (new AuthorityClientMetadataKeys.RequiresAirGapSealConfirmation) are gated. When enabled, /token rejects those requests with invalid_client until:

  1. timestamp inside the evidence file is newer than maxEvidenceAge.
  2. health.authority/signer/attestor.status are all pass (each requirement can be toggled off via the options above).
  3. egressProbe.status equals pass, confirming outbound traffic was blocked during the harness run.

Audit events now include airgap.sealed=<state> where <state> is failure:<code> (for example failure:evidence_missing) or confirmed:<rfc3339 timestamp>. Token validation spans also emit the authority.sealed_mode activity tag with the same value, so dashboards can alarm when evidence goes stale.

7. Configuration Reference

SectionKeyDescriptionNotes
RootissuerAbsolute HTTPS issuer advertised to clients.Required. Loopback HTTP allowed only for development.
TokensaccessTokenLifetime, refreshTokenLifetime, etc.Lifetimes for each grant (access, refresh, device, authorization code, identity).Enforced during issuance; persisted on each token document.
Storagestorage.connectionStringPostgreSQL connection string.Required even for tests; offline kits ship snapshots for seeding.
Signingsigning.enabledEnable JWKS/revocation signing.Disable only for development.
Signingsigning.algorithmSigning algorithm identifier.Currently ES256; additional curves can be wired through crypto providers.
Signingsigning.keySourceLoader identifier (file, vault, custom).Determines which IAuthoritySigningKeySource resolves keys.
Signingsigning.keyPathRelative/absolute path understood by the loader.Stored as-is; rotation request should keep it in sync with filesystem layout.
Signingsigning.activeKeyIdActive JWKS / revocation signing key id.Exposed as kid in JWKS and bundles.
Signingsigning.additionalKeys[].keyIdRetired key identifier retained for verification.Manager updates this automatically after rotation; keep YAML aligned.
Signingsigning.additionalKeys[].sourceLoader identifier per retired key.Defaults to signing.keySource if omitted.
Securitysecurity.rateLimitingFixed-window limits for /token, /authorize, /internal/*.See docs/security/rate-limits.md for tuning.
Bootstrapbootstrap.apiKeyShared secret required for /internal/*.Only required when bootstrap.enabled is true.

7.1 Sender-constrained clients (DPoP & mTLS)

Rollout tracker: see docs/security/dpop-mtls-rollout.md for phase gates tied to AUTH-DPOP-11-001 and AUTH-MTLS-11-002.

Authority now understands two flavours of sender-constrained OAuth clients:

Both modes persist additional metadata in authority_tokens: senderConstraint records the enforced policy, while senderKeyThumbprint stores the DPoP JWK thumbprint or mTLS certificate hash captured at issuance. Downstream services can rely on these fields (and the corresponding cnf claim) when auditing offline copies of the token store.

7.2 Policy Engine clients & scopes

Policy Engine v2 introduces dedicated scopes and a service identity that materialises effective findings. Configure Authority as follows when provisioning policy clients:

ClientScopesNotes
policy-engine (service)policy:run, findings:read, effective:writeMust include properties.serviceIdentity: policy-engine and a tenant. Authority rejects effective:write tokens without the marker or tenant.
policy-cli / automationpolicy:read, policy:author, policy:review, policy:simulate, findings:read (optionally add policy:approve / policy:operate / policy:activate for promotion pipelines)Keep scopes minimal; reroll CLI/CI tokens issued before 2025‑10‑27 so they drop legacy scope names and adopt the new set.
UI/editor sessionspolicy:read, policy:author, policy:simulate (+ reviewer/approver/operator scopes as appropriate)Issue tenant-specific clients so audit and rate limits remain scoped.

Sample YAML entry:

  - clientId: "policy-engine"
    displayName: "Policy Engine Service"
    grantTypes: [ "client_credentials" ]
    audiences: [ "api://policy-engine" ]
    scopes: [ "policy:run", "findings:read", "effective:write" ]
    tenant: "tenant-default"
    properties:
      serviceIdentity: "policy-engine"
    senderConstraint: "dpop"
    auth:
      type: "client_secret"
      secretFile: "../secrets/policy-engine.secret"

Compliance checklist:

7.3 Orchestrator roles & scopes

Role / ClientScopesNotes
Orch.Viewer roleorch:readRead-only access to Orchestrator dashboards, queues, and telemetry.
Orch.Operator roleorch:read, orch:operateIssue short-lived tokens for control actions (pause/resume, retry, sync). Token requests must include operator_reason (≤256 chars) and operator_ticket (≤128 chars); Authority rejects requests missing either value and records both in audit events.
Orch.Admin roleorch:read, orch:operate, orch:quota, orch:backfillManage tenant quotas, burst ceilings, and historical backfill allowances. Quota tokens must include quota_reason (≤256 chars) and may include quota_ticket (≤128 chars); backfill tokens must include both backfill_reason (≤256 chars) and backfill_ticket (≤128 chars). Authority records all values in audit trails.

Token request example via client credentials:

curl -u orch-operator:s3cr3t! \
  -d 'grant_type=client_credentials' \
  -d 'scope=orch:operate' \
  -d 'operator_reason=resume source after maintenance' \
  -d 'operator_ticket=INC-2045' \
  https://authority.example.com/token

Tokens lacking operator_reason or operator_ticket receive invalid_request; audit events (authority.client_credentials.grant) surface the supplied values under request.reason and request.ticket for downstream review. CLI clients set these parameters via Authority.OperatorReason / Authority.OperatorTicket (environment variables STELLAOPS_ORCH_REASON and STELLAOPS_ORCH_TICKET).

Quota administration tokens follow the same pattern:

curl -u orch-admin:s3cr3t! \
  -d 'grant_type=client_credentials' \
  -d 'scope=orch:quota' \
  -d 'quota_reason=temporary burst for release catch-up' \
  -d 'quota_ticket=CHG-8821' \
  https://authority.example.com/token

CLI automation should supply these values via Authority.QuotaReason / Authority.QuotaTicket (environment variables STELLAOPS_ORCH_QUOTA_REASON and STELLAOPS_ORCH_QUOTA_TICKET). Missing quota_reason yields invalid_request; when provided, both reason and ticket are captured in audit properties (quota.reason, quota.ticket).

Backfill run tokens extend the same pattern:

curl -u orch-admin:s3cr3t! \
  -d 'grant_type=client_credentials' \
  -d 'scope=orch:backfill' \
  -d 'backfill_reason=rebuild historical findings for tenant-default' \
  -d 'backfill_ticket=INC-9905' \
  https://authority.example.com/token

CLI clients configure these values via Authority.BackfillReason / Authority.BackfillTicket (environment variables STELLAOPS_ORCH_BACKFILL_REASON and STELLAOPS_ORCH_BACKFILL_TICKET). Tokens missing either field are rejected with invalid_request; audit events store the supplied values as backfill.reason and backfill.ticket.

7.4 Delegated service accounts

StellaOps Authority issues short-lived delegated tokens for service accounts so automation can operate on behalf of a tenant without sharing the underlying client identity.

Configuration summary

delegation:
  quotas:
    maxActiveTokens: 50
  serviceAccounts:
    - accountId: "svc-observer"
      tenant: "tenant-default"
      displayName: "Observability Exporter"
      description: "Delegated identity used by Export Center to read findings."
      enabled: true
      allowedScopes: [ "jobs:read", "findings:read" ]
      authorizedClients: [ "export-center-worker" ]

tenants:
  - name: "tenant-default"
    delegation:
      maxActiveTokens: 25

Bootstrap administration

Bootstrap operators can inspect and rotate delegated identities via the internal API group:

GET  /internal/service-accounts?tenant={tenantId}
GET  /internal/service-accounts/{accountId}/tokens
POST /internal/service-accounts/{accountId}/revocations

Requests must include the bootstrap API key header (X-StellaOps-Bootstrap-Key). Listing returns the seeded accounts with their configuration; the token listing call shows currently active delegation tokens (status, client, scopes, actor chain) and the revocation endpoint supports bulk or targeted token revocation with audit logging.

Bootstrap seeding reuses the existing PostgreSQL id/created_at values. When Authority restarts with updated configuration it upserts rows without mutating immutable fields, avoiding duplicate or conflicting service-account records.

Requesting a delegated token

curl -u export-center-worker:s3cr3t \
  -d 'grant_type=client_credentials' \
  -d 'scope=jobs:read findings:read' \
  -d 'service_account=svc-observer' \
  https://authority.example.com/token

Optional delegation_actor metadata appends an identity to the actor chain:

-d 'delegation_actor=pipeline://exporter/step/42'

Token shape & observability

Delegated tokens still honour scope validation, tenant enforcement, sender constraints (DPoP/mTLS), and fresh-auth checks.

8. Offline & Sovereign Operation

9. Operational Checklist

For plug-in specific requirements, refer to Authority Plug-in Developer Guide. For revocation bundle validation workflow, see Authority Revocation Bundle.