Concelier connector credentials — operator acquisition guide
Audience: operators wiring Concelier to vendor-gated advisory feeds (
cisco,microsoft,cve) and optional GHSA rate-limit tokens. Companion docs:
credentials-matrix.md— full per-source mechanism inventory (PAT / OAuth / API key / public).RFC-0001-connector-credentials-storage.md— Console UI + Platform storage design that this guide complements.SPRINT_20260505_037— currentskip-with-warningbehavior when credentials are absent.SPRINT_20260505_026— operator-decision workflow.
UI path (Console)
Operators can now provision connector credentials from the Console at /admin/connectors/credentials(redirected internally to /console-admin/connector-credentials). The page is gated by two Authority permissions added in SPRINT_20260505_045:
| Permission | Grants |
|---|---|
connector:credentials:read | List + detail view; never returns plaintext or authref:// URIs (RFC-0001 §6.4). |
connector:credentials:write | Create, rotate, revoke. Platform-admin role inherits both by default. |
Behaviour notes:
Send-only. Plaintext is encrypted at rest via
IAeadAlgorithm(AES-256-GCM by default) with AAD bindingconnector-cred|tenant|credential_id|kind— re-using a ciphertext across rows or kinds fails decryption (RFC-0001 §3.3).GETreturns only metadata.Rotation. Rotating produces a new active version while marking the old version
rotatingfor the grace window (24h default, per-credential override viametadata.grace_window_seconds; RFC §5.1). Both versions remain resolvable so in-flight cron ticks finish cleanly.Expiry. Once
expires_atpasses, the credential enters a per-tenant-configurable grace window (tenant_settings.credential_expiry_grace_days, default 7d, range 1…30 enforced byck_tenant_settings_grace_range) during which it still resolves with WARN logs. After the grace,ConnectorCredentialExpirySweeper(a Platform-WebService hosted service ticking hourly by default) transitions the row tostate='expired', zeroes the ciphertext, emits an audit row taggedplatform.connector_credentials.expire, and publishes a Valkeyconnector-credentials-changedevent withaction="expire"so Concelier / Excititor caches drop the stale value before their 30 s TTL elapses. The next connector tick falls back to skip-with-WARN (Sprint 20260505_037 contract; RFC §5.3).Operator override. Change a tenant’s grace window with:
INSERT INTO platform.tenant_settings (tenant_id, credential_expiry_grace_days) VALUES ('<tenant-uuid>', 14) ON CONFLICT (tenant_id) DO UPDATE SET credential_expiry_grace_days = EXCLUDED.credential_expiry_grace_days, updated_at = now();Values outside
[1, 30]are rejected by the CHECK constraint. Sweeper cadence is configurable viaPlatform:ConnectorCredentials:SweepInterval(default 1 h) and the advisory-log lead time viaPlatform:ConnectorCredentials:WarnLeadTime(default 7 d, matching the RFC §5.3 amber badge window).Audit. Every create / rotate / revoke / runtime read writes a
UnifiedAuditEventviaIAuditEventEmitter. Audit payloads strictly exclude ciphertext, plaintext, ANDauthref://URIs (RFC §7).Env-var fallback remains. Connectors resolve in the order DB → env var → skip-with-WARN (RFC §8.2). Operators who have not migrated keep working without UI interaction; the env-var fallback can be disabled per tenant via
platform.environment_settingskeyconnectorCredentials:envVarFallback=false.
REST surface (mounted at /api/v1/connector-credentials inside the Platform service, exposed to Console as /platform/api/v1/connector-credentials):
| Verb | Path | Required scope | Purpose |
|---|---|---|---|
GET | / | connector:credentials:read | List credentials (metadata only). |
GET | /{id} | connector:credentials:read | Detail view (metadata only). |
POST | / | connector:credentials:write | Create. Send-only. |
POST | /{id}/rotate | connector:credentials:write | Rotate with optional grace override. |
DELETE | /{id} | connector:credentials:write | Revoke immediately (row retained for audit). |
POST | /{id}/verify | connector:credentials:read | Verify button (SPRINT_20260512_021 / RFC §6.2 step 4). Confirms the credential decrypts and is in a resolvable state. Returns { ok, reason, state, checkedAt } — never plaintext. |
POST | /resolve | connector:credentials:read | Connector runtime resolution. Returns the decrypted plaintext base64-encoded for the calling tenant + named connector. Concelier / Excititor connectors call this internally through IRemoteCredentialResolver; operators do not invoke it directly. Emits the minute-batched audit row per RFC §7. |
Verify button workflow
The Console list view exposes a Verify button on every row that is in active or rotating state. The button now picks between two probes:
- Vendor dry-run (
SPRINT_20260512_032, default when supported) — Concelier runs the connector’s real auth path against the upstream vendor. - Platform metadata (RFC-0001 §6.2 step 4, fallback) — Platform decrypts the row and runs the state-machine check. No vendor I/O.
Vendor dry-run (active rows on supported connectors)
POST /concelier/api/v1/connectors/{connectorId}/dry-run-credential
Authorization: Bearer <token with concelier:credentials:dry-run>
X-StellaOps-TenantId: <tenant>
Content-Type: application/json
{ "credentialId": "<guid>" }
Response shape:
{
"ok": true,
"connectorId": "concelier:ghsa",
"credentialId": "fbe1d6c4-...",
"vendorReachable": true,
"authAccepted": true,
"latencyMs": 842,
"reason": null,
"evidence": "GitHub GraphQL viewer.login=octocat."
}
Binding contract (enforced by the endpoint, see docs/architecture/proposal-concelier-credential-dryrun.md):
- Exactly one upstream HTTP call. Implementations cannot retry; a transient failure is a failed dry-run, not a stall.
- No persistence. The endpoint never writes to advisories, source state, or credential audit beyond the standard
runtime-readrow emitted by Platform’s/resolve(RFC §7). - Active-only. Dry-run declines rotating / expired rows with
409 Conflict— operators wanting a metadata-only check on those rows use the Platform/verifypath, which the Console falls back to automatically. - 10 s hard timeout enforced by a linked
CancellationTokenSourceon the endpoint side. - Connector capability gate. Connectors with no
IConnectorDryRunregistration get501 Not Implementedwith an explicit reason. As of Sprint 032 (phase II): GHSA, Cisco, MSRC, CVE, Red Hat Hydra, and Excititorredhat-csafall ship handlers.
| Connector | Probe shape (Sprint 032) |
|---|---|
concelier:ghsa | GraphQL query { viewer { login } } against /graphql (auth-only). |
concelier:cisco | OAuth2 client_credentials grant against the configured TokenEndpoint. |
concelier:msrc | Azure AD client_credentials grant against https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token (same path MsrcTokenProvider uses on every cron tick). 400/401 bodies surface AADSTSxxxxx error codes verbatim in reason. |
concelier:cve | Authenticated GET /cve?resultsPerPage=1 against the configured BaseEndpoint with the CVE-API-ORG / CVE-API-USER / CVE-API-KEY triplet (same headers used by the production fetch). |
concelier:redhat | Authenticated GET https://api.access.redhat.com/management/v1/subscriptions/?limit=1 with Authorization: Bearer <PAT>. The RHSM subscription endpoint is genuinely PAT-gated, unlike the anonymous-accessible Hydra security-data path. |
excititor:redhat-csaf | Authenticated GET <MetadataUri> against the configured CSAF provider-metadata document (default: https://security.access.redhat.com/data/csaf/v2/provider-metadata.json) with Authorization: Bearer <PAT>. The metadata endpoint is anonymous-accessible today; the probe still exercises the CSAF directory host and rejects malformed PATs (401). |
Platform metadata verify (fallback for rotating)
POST /platform/api/v1/connector-credentials/{id}/verify
Authorization: Bearer <token with connector:credentials:read>
Returns { ok, reason, state, checkedAt }. This was the only path prior to Sprint 032; it stays as the fallback for rows the dry-run endpoint declines (rotating / expired) and for connectors that have not yet shipped an IConnectorDryRun. Plaintext never leaves the Platform process.
Why the split
The pre-Sprint-032 button did not call the upstream vendor — it only proved the row decrypted. Operators have asked for a true probe (“given this stored credential, does the connector authenticate against the upstream vendor today?”) which is what the new endpoint delivers. The Console picks automatically, so operators do not have to know which path is in play; the result banner spells out whether the probe was a vendor call or metadata-only.
Runtime resolution chain (RFC §8.2)
Connectors resolve their credential in this order on every overlay run:
- Platform DB —
IRemoteCredentialResolverreturns the decrypted value for(connectorId, currentTenant). 30 s in-process cache with eager Valkeyconnector-credentials-changedinvalidation (SPRINT_20260512_021). - Env var / DB source-settings — the existing
AdvisorySourceRuntimeOverlay<T>dictionary path (CONCELIER_concelier__sources__ghsa__ApiToken, etc.). - Skip-with-WARN — Sprint 037 contract. GHSA is the exception: anonymous mode is a legitimate operating point.
Un-tenanted ticks (no IStellaOpsTenantAccessor.TenantId) skip step 1 and fall straight through to env var per RFC §4.
Behavior with no credentials: credential-gated connectors below log a single
WARNon the first cron tick after startup and then no-ops on every subsequent tick.vuln.source_states.error_countdoes not climb. You can leave them un-wired indefinitely without log noise or false-positive health-check failures. GHSA is different: public global advisories run anonymously andApiTokenis optional.Verification command (after wiring credentials, recreate concelier and run):
bash devops/compose/scripts/stella-connectors.sh pipeline <source>A wired credential produces non-zero
pre/postrow deltas in the output. An un-wired credential keeps the existing skip-with-WARN log.
How env vars map to config
Concelier services bind through AddStellaOpsDefaults with EnvironmentPrefix = "CONCELIER_". A config key concelier:sources:cisco:ClientId resolves from the environment variable CONCELIER_concelier__sources__cisco__ClientId (env prefix CONCELIER_, section path with : replaced by __). Operators set these in devops/compose/env/stellaops.env (or .env) and recreate the concelier container.
Example for all four below:
# devops/compose/env/stellaops.env (or .env)
# --- GHSA ---
CONCELIER_concelier__sources__ghsa__ApiToken=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# --- Microsoft (MSRC) ---
CONCELIER_concelier__sources__microsoft__TenantId=00000000-0000-0000-0000-000000000000
CONCELIER_concelier__sources__microsoft__ClientId=00000000-0000-0000-0000-000000000000
CONCELIER_concelier__sources__microsoft__ClientSecret=...
# --- Cisco ---
CONCELIER_concelier__sources__cisco__ClientId=...
CONCELIER_concelier__sources__cisco__ClientSecret=...
# --- CVE.org (MITRE CVE Services) ---
CONCELIER_concelier__sources__cve__ApiOrg=YourOrgShortName
CONCELIER_concelier__sources__cve__ApiUser=your.user@example.com
CONCELIER_concelier__sources__cve__ApiKey=...
Apply with:
docker compose -f devops/compose/docker-compose.stella-ops.yml up -d --no-deps --force-recreate concelier
1. GHSA (GitHub Security Advisories)
| Field | Value |
|---|---|
| Source key | ghsa |
| Connector path | src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/ |
| Auth mechanism | Optional GitHub Personal Access Token (PAT) or GitHub App token |
| Cost | Free |
| Rate limit | Anonymous REST quota for public global advisories; authenticated REST quota when ApiToken is configured; connector warns when remaining drops below 500 |
1.1 Where to register
- Sign in to https://github.com.
- Open Settings → Developer settings → Personal access tokens → Tokens (classic) or Fine-grained tokens.
- Direct link: https://github.com/settings/tokens.
- New token with the scopes below.
1.2 Required scopes / permissions
- No token is required for public global advisories.
- Classic PAT: use the minimum organization-approved read scope for public metadata when a token is configured for higher rate limits.
- Fine-grained PAT: Metadata: read is sufficient for public advisory access when a token is configured.
1.3 Optional env vars to set
CONCELIER_concelier__sources__ghsa__ApiToken=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Other optional settings (defaults usually sufficient):
CONCELIER_concelier__sources__ghsa__BaseEndpoint=https://api.github.com/
CONCELIER_concelier__sources__ghsa__PageSize=10
CONCELIER_concelier__sources__ghsa__MaxPagesPerFetch=1
1.4 Verification
bash devops/compose/scripts/stella-connectors.sh pipeline ghsa
Expected on success: pre=0 (or prior count), post>pre after fetch -> parse -> map. Expected without creds: the connector still runs against public global advisories; failures should be treated as network, rate-limit, endpoint, or parser issues rather than a missing-token skip.
2. Microsoft Security Update Guide (MSRC)
| Field | Value |
|---|---|
| Source key | microsoft |
| Connector path | src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Msrc/ |
| Auth mechanism | OAuth2 client_credentials against Azure AD (Entra ID) |
| Cost | Free (requires an Azure AD tenant; the API itself has no charge) |
| Rate limit | Connector enforces RequestDelay=250ms, PageSize<=500, FailureBackoff=5min |
2.1 Where to register
- Sign in to https://portal.azure.com with an account that can register apps in your directory.
- Open Microsoft Entra ID → App registrations → New registration.
- Give the app a name (e.g.
stellaops-concelier-msrc). Account types: Accounts in this organizational directory only is sufficient; no redirect URI is needed (client_credentials flow). - After creation, open the app:
- Certificates & secrets → New client secret → copy the Value (this is the
ClientSecret; it is only shown once). - API permissions → Add a permission → APIs my organization uses → search Microsoft Security Update Guide → select the Application permission for the SUG API → Grant admin consent.
- Overview → copy Application (client) ID (the
ClientId) and Directory (tenant) ID (theTenantId).
- Certificates & secrets → New client secret → copy the Value (this is the
- MSRC SUG API onboarding reference: https://api.msrc.microsoft.com/sug/v2.0/swagger and Microsoft’s “Security Update Guide REST API” docs.
2.2 Required scopes / permissions
- API scope:
api://api.msrc.microsoft.com/.default(the connector default). Admin consent is required.
2.3 Env vars to set
CONCELIER_concelier__sources__microsoft__TenantId=00000000-0000-0000-0000-000000000000
CONCELIER_concelier__sources__microsoft__ClientId=00000000-0000-0000-0000-000000000000
CONCELIER_concelier__sources__microsoft__ClientSecret=...
Optional (default usually sufficient):
CONCELIER_concelier__sources__microsoft__Scope=api://api.msrc.microsoft.com/.default
2.4 Verification
bash devops/compose/scripts/stella-connectors.sh pipeline microsoft
Expected without creds: MSRC fetch skipped: missing TenantId/ClientId/ClientSecret.
3. Cisco PSIRT (OpenVuln API)
| Field | Value |
|---|---|
| Source key | cisco |
| Connector path | src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco/ |
| Auth mechanism | OAuth2 client_credentials against https://id.cisco.com/oauth2/default/v1/token |
| Cost | Free (requires Cisco.com account + entitlement approval — typically same-day for organizations with active Cisco contracts) |
| Rate limit | Connector enforces PageSize<=100, RequestDelay=250ms, RequestTimeout=30s, retry attempts=5 |
3.1 Where to register
- Create or sign in with a Cisco.com account at https://id.cisco.com/.
- Open the Cisco API Console at https://apiconsole.cisco.com/.
- My Apps & Keys → Register a New App:
- Name:
stellaops-concelier-cisco - OAuth2.0 Credentials Grant Type: Client Credentials
- Select APIs: tick OpenVuln API
- Name:
- Submit. Approval is required from Cisco before the entitlement becomes active. After approval the app page exposes the
Client IDandClient Secret.
3.2 Required scopes / permissions
- OpenVuln API entitlement (approved by Cisco). No additional scope string is required — the token is bound to the entitled APIs.
3.3 Env vars to set
CONCELIER_concelier__sources__cisco__ClientId=...
CONCELIER_concelier__sources__cisco__ClientSecret=...
Optional (defaults from CiscoOptions):
CONCELIER_concelier__sources__cisco__TokenEndpoint=https://id.cisco.com/oauth2/default/v1/token
CONCELIER_concelier__sources__cisco__BaseUri=https://apix.cisco.com/advisories/v2/
3.4 Verification
bash devops/compose/scripts/stella-connectors.sh pipeline cisco
Expected without creds: Cisco fetch skipped: missing ClientId/ClientSecret.
4. CVE.org (MITRE CVE Services)
| Field | Value |
|---|---|
| Source key | cve |
| Connector path | src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cve/ |
| Auth mechanism | No credential for public cvelistV5; optional API key for CVE Services; offline SeedDirectory fallback |
| Cost | Free (CVE Program is publicly funded; key issuance is gated to organizations participating in CVE Numbering Authorities (CNAs) or approved CVE consumers) |
| Rate limit | Self-throttled by RequestDelay=250ms, PageSize<=500, FailureBackoff=10min |
4.1 Public no-credential default
The default Stella Ops Mirror path reads the official public CVE List v5 raw JSON repository at https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/. The fetch phase reads deltaLog.json, then downloads changed CVE-*.json records. No CVE Services credentials are required for this mode.
4.2 Where to register for optional CVE Services API use
- Read the CVE Services overview at https://www.cve.org/AllResources/CveServices.
- Apply for an account with the CVE Program through your CNA or via the contact form at https://www.cve.org/ResourcesSupport/Contact.
- Once approved, your CNA admin issues:
ApiOrg— your organisation short nameApiUser— your account email/handleApiKey— long opaque string (rotate via the CVE Services UI)
- Test endpoint: https://cveawg.mitre.org/api/cve/CVE-2024-0001 should return JSON when called with the
CVE-API-ORG,CVE-API-USER,CVE-API-KEYheaders.
4.2.1 Offline fallback (no API key needed)
For air-gap deployments or operators who consume the CVE List via git clone https://github.com/CVEProject/cvelistV5, set SeedDirectory to the local path containing the year-partitioned JSON. The connector then ingests from disk without any HTTP call. This is the recommended path for offline/air-gap operators.
4.3 Required scopes / permissions
CVE Services uses the org/user/key triple as both authentication and authorization; no separate scope string. CNA write permissions are not required for read ingestion.
4.4 Env vars to set
For authenticated CVE Services operation:
CONCELIER_concelier__sources__cve__ApiOrg=YourOrgShortName
CONCELIER_concelier__sources__cve__ApiUser=your.user@example.com
CONCELIER_concelier__sources__cve__ApiKey=...
For offline operation (pick this path for air-gap):
CONCELIER_concelier__sources__cve__SeedDirectory=/var/lib/concelier/cvelistV5
Optional (defaults from CveOptions):
CONCELIER_concelier__sources__cve__CveListV5Endpoint=https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/
CONCELIER_concelier__sources__cve__BaseEndpoint=https://cveawg.mitre.org/api/
CONCELIER_concelier__sources__cve__PageSize=500
CONCELIER_concelier__sources__cve__InitialBackfill=30.00:00:00
4.5 Verification
bash devops/compose/scripts/stella-connectors.sh pipeline cve
Expected without creds: the connector attempts public cvelistV5 first. It only falls back to SeedDirectory when the public delta source is unavailable or yields no records.
Operator follow-ups (out of scope here)
- Setup-wizard / Console UI for pasting credentials with encrypted persistence is tracked in
SPRINT_20260505_045_Console_connector_credentials_ui.md. - Per-row enable/defer/decline decisions (one operator pass across the full credentials matrix) are tracked in
SPRINT_20260505_026taskB-CRED-002.
Audit notes
This document was authored from local sources only:
*Options.csdefaults read from each connector library undersrc/Concelier/__Libraries/StellaOps.Concelier.Connector.<Vendor>/.credentials-matrix.md(Sprint 20260505_026, B-CRED-001 output).stella-connectors.shpipelinecommand verified againstdevops/compose/scripts/stella-connectors.sh.
No external URLs were fetched during authoring (offline-first posture). The vendor URLs above are documented in connector source comments and / or credentials-matrix.md; operators should verify the URLs are current at credential-acquisition time.
