Design Proposal: Concelier Credential Dry-Run Endpoint
Audience: Concelier/Excititor connector engineers and Platform reviewers weighing how to add a live vendor-credential probe. This is a proposal (a recommendation under review), not an accepted decision.
Status: Proposed Date: 2026-05-12 Sprint: docs/implplan/SPRINT_20260512_021_Concelier_Excititor_credentials_resolver_wiring.md — TASK-021-08 Upstream RFC: RFC-0001 — Connector Credentials Storage (§6.2 step 4) Authors: Concelier Connectors / Platform
Context
The Console Verify button (TASK-021-04, DONE) calls Platform’s POST /platform/api/v1/connector-credentials/{id}/verify. That endpoint runs a metadata-only path: it AEAD-decrypts the row using the per-tenant AAD, confirms the rotation state machine is sound, and returns { ok, reason, state }. It does not dial the upstream vendor API.
The follow-up Operators have asked for is a true probe — i.e. “given this stored credential, does the connector authenticate successfully against the upstream vendor today?” That requires Concelier (or Excititor) execution because Platform must remain vendor-agnostic.
Concelier already exposes POST /api/v1/advisory-sources/{sourceId}/check (SourceManagementEndpointExtensions.cs:372) which exercises connector connectivity but uses whatever overlay the connector currently resolves — there is no way to ask “exercise the connector with a specific credentialId instead of whatever the current overlay picks.”
Options
Option A — Extend /{sourceId}/check with ?credentialId=
POST /api/v1/advisory-sources/{sourceId}/check?credentialId={uuid} forces the connector to resolve the named credential row for the duration of the call and runs the existing connectivity probe.
- Pros: smaller surface; existing scope (
SourcesManagePolicy) already covers it; UI reuses one endpoint; Excititor mirror is the same one-line shape. - Cons:
/checktoday is mutative — it auto-enables healthy sources and disables failed ones (CheckAllAndPersistAsync). The per-source variant callsCheckSourceConnectivityAsyncwhich is read-only, but the verb naming is shared. Passing acredentialIdmust NOT trigger persistence side-effects; we would need an explicitdryRun=truebranch (parameter pollution).
Option B — New /{sourceId}/dry-run-credential verb
POST /api/v1/advisory-sources/{sourceId}/dry-run-credential with body { credentialId: "<uuid>" }.
- Pros: explicit “no side-effects” contract baked into the verb name; separate audit action (
concelier.dryrunvsconcelier.verify); separate scope possible if we later want to gate vendor-probe more tightly than connectivity-check. - Cons: +1 API surface; both Concelier and Excititor must mirror it; Console must pick which endpoint to call based on capability.
Recommendation
Option B — new /{sourceId}/dry-run-credential verb.
The /check endpoint carries persistence semantics that are wrong for the Verify use case (a Verify click must never enable/disable a source). Bolting ?credentialId= + an implicit dryRun=true onto /check collapses two distinct user intents (operator says “is this source healthy enough to auto-toggle?” vs. operator says “does this specific credential authenticate?”) into one URL, and the audit trail (concelier.verify on advisory_source resource type) would conflate them. Explicit verb keeps the audit log readable and the scope-tightening option open.
Endpoint contract sketch
POST /api/v1/advisory-sources/{sourceId}/dry-run-credential
Authorization: Bearer <token with concelier:credentials:dry-run>
Content-Type: application/json
Request:
{
"credentialId": "fbe1d6c4-...-...", // required, UUID
"timeoutSeconds": 15 // optional, default 15, max 60
}
Response 200:
{
"ok": true | false,
"reason": null | "vendor_4xx" | "vendor_5xx" | "timeout" | "tls_handshake" | "scope_missing" | ...,
"vendorStatus": 200, // HTTP status from vendor (when applicable)
"elapsedMs": 842,
"credentialState": "active" | "rotating",
"probedAt": "2026-05-12T19:04:11Z"
}
Response 404: credentialId not found / not visible to tenant
Response 403: scope missing
Response 409: credential is revoked or pending (cannot dry-run)
Response 400: source does not support credentials / no probe defined
Hygiene rules (binding)
- Never returns plaintext — same posture as Platform
/verify(RFC §7). - No persistence side-effects — must not enable/disable the source, must not flip rotation state, must not increment last-success-at.
- Read-only audit row —
concelier.dryrunaction onconnector_credentialresource type, withcredentialIdandsourceIdin metadata. No plaintext, no AAD, no ciphertext bytes. - Scope: new
concelier:credentials:dry-run. Distinct fromconnector:credentials:read(which is a Platform-side read) andconcelier.sources.manage(which is mutative). - Rate limit: per-tenant 10 req/min — vendor probes are expensive and operators should not loop them.
Impact on existing /check
None. /{sourceId}/check keeps its current shape and semantics. ?credentialId= is not added. Operators continue to use /check for the auto-enable/disable workflow and the new /dry-run-credential verb for credential-specific probes.
Implementation surface
src/Concelier/StellaOps.Concelier.WebService/Extensions/SourceManagementEndpointExtensions.cs— register endpoint, wire to newIConnectorCredentialDryRunService.src/Concelier/__Libraries/StellaOps.Concelier.Connector.Common/Credentials/—IConnectorCredentialDryRunnerinterface; per-connector implementations register against it.src/Excititor/StellaOps.Excititor.WebService/— mirror endpoint.src/Authority/.../StellaOpsScopes.cs— declareconcelier:credentials:dry-runandexcititor:credentials:dry-run.src/Web/StellaOps.Web/.../console-admin/connector-credentials/— Verify button switches to the new endpoint when capability flagvendorDryRunSupportedis true; falls back to the metadata-only Platform/verifyotherwise.
Open questions
- Which connectors expose a cheap probe? GHSA, Cisco, MSRC have obvious
GET /api/v1/me-shaped probes. Red Hat Hydra and CVE.org may not. The interface should let a connector returnnot_supportedcleanly rather than running a 30-second list-query fallback. - Excititor naming: dry-run-credential makes less sense for VEX mirrors that are anonymous-only. Likely no-op +
not_supportedfor the redhat-csaf connector.
Decision log
- Deferred until at least three connectors (GHSA + 2 others) have shipped the resolver wiring (TASK-021-07) so we have real vendor-probe shapes to validate the interface against.
