Skip to content

Veltro suite golden paths (canonical)

Source of truth: docs/golden-paths.md (synced at build time — edit the original, not this copy). The end-to-end flows that define the suite, each classified shipped (implemented) or target (approved, not yet built). This is the canonical summary; per-edge source evidence and the full eight-trace analysis live in the dated evidence snapshot docs/discovery/GOLDEN-PATHS.md. Terms: CONTEXT.md. Authority: the ADRs in docs/adr/.

1. Single sign-on → each product via native RBAC — shipped

Section titled “1. Single sign-on → each product via native RBAC — shipped”

Browser signs in once at Veltro (Auth.js JWE cookie with session_contract_version = 2, immutable suite_org_id, a transition-only matching org_id, and apps claims). VectorFlow proves active membership before minting; Sigmatic and Warden resolve the signed suite identifier to local Organization rows before JIT or authorization, then enforce product-native roles from apps.<product> (including Warden approver). Host and slug remain routing only, and one origin keeps the cookie host-only. The organization/session v2 rollout shipped in suite 0.4.3 (#59/#144), including non-default organizations; legacy v1/org_id remains only for the bounded compatibility window documented in docs/suite-org-v2-rollout.md. See ADR-008 and ADR-003. Target: 60-second grant revocation across active sessions (#129).

2. VectorFlow ships telemetry into Sigmatic — shipped

Section titled “2. VectorFlow ships telemetry into Sigmatic — shipped”

The destination picker uses sigmatic:index-config:read as the signed-in person. Identity checks their current Sigmatic grant for the environment’s owning team before issuing a token. The browser’s VectorFlow assertion carries only VectorFlow authority, so it can’t answer whether the person also has Sigmatic access.

A Sigmatic-sink pipeline node resolves the target Data Source’s ingest token over the suite-service external:read plane at delivery, then the agent POSTs to /api/logs/{suffix}/queue. See ADR-004 integration A.

3. Sigmatic detects and raises an Alert — shipped

Section titled “3. Sigmatic detects and raises an Alert — shipped”

Ingested events percolate against deployed Sigma/correlation rules and MISP IOCs → an Alert on the canonical 5-level Severity (ADR-007). The portable golden path (scripts/seed-demo.sh + e2e/golden-path.spec.ts) now proves the second detection source too: a known IOC seeded in a MISP fixture produces its IOC alert, escalates into the same Warden case as the seeded Sigma rule (no duplicate case or alert lineage), and records the MISP sighting back onto the seeded observable (#466). Drift/target: the detection pipeline is currently implemented three times (sync/queued/pull) and must converge on one shared semantic pipeline while transport/batching/retries/checkpoints stay ingress-specific (#140). Governed lifecycle (shadow/canary, version provenance) is roadmap (#58).

4. Alert → Warden Case — shipped (two lanes)

Section titled “4. Alert → Warden Case — shipped (two lanes)”

Sigmatic escalates over two lanes: POST /api/webhooks/sigmatic (always-Case, manual escalate) and POST /api/events/sigmatic (case-optional, automatic), both bearing X-Warden-Token. Warden dedups by correlation key, creates/appends the Case, and threads back {case_id, case_key, created}. Warden is the sole case authority, and the drift is now closed: Sigmatic’s local Cases surface is gone at the pinned CHAD_VERSION=0.43.2 (no backend/app/api/cases.py, no frontend/src/pages/Cases.tsx, migration backend/alembic/versions/20260807b_drop_legacy_cases.py). Sigmatic no longer stores or renders a case; the case-facing endpoints it keeps are read/reference only: the deep link GET /api/warden/alert/{alert_id}/linked-case and the analyst pull GET /api/warden/related-cases (backend/app/api/warden.py:270,581, mounted under /api), the escalation POST /api/alerts/{alert_id}/escalate-to-warden (backend/app/api/alerts.py:1339, which stamps the returned key via set_warden_case at :1404), and the Warden→Sigmatic writeback POST /api/external/alerts/{alert_id}/case-ref (backend/app/api/external.py:990) (#135 shipped).

5. Warden runs an approval-aware Action — shipped

Section titled “5. Warden runs an approval-aware Action — shipped”

A binding enqueues a Run of an immutable runbook version; a step with an approval: gate (or a default-deny risk-gate trip) freezes intent into an Approval and pauses; resume is digest-verified. Approve requires approve_runbooks on the team and membership in Approval.required_role. The three items this section listed as Targets have landed at the pinned WARDEN_VERSION=0.11.7 (f5f065fa), so they are no longer targets: requester-distinct four-eyes is enforced fail-closed by backend/app/services/approval_policy.py (four_eyes_required at :100, asserted at :194-209, #132 closed); the approval-gated reversible brute-force response is runbooks/bruteforce-response.yml version: 2 with its approval: gate (:39), ttl_seconds (:48) and a compensating unblock previewed before approval (#139 closed); and TTL-bound reversible containment is proven portably by crowdsec-real-temporary-block — see §10 for that evidence and its limits, and note that runbook is deliberately not approval-gated (no approval: key), so the two seams are separate proofs. Remaining target: live enforcement — response_ip_block is still a truthful no-effect tracer (#36/#37).

6. Warden writeback → Sigmatic — shipped (attribution target)

Section titled “6. Warden writeback → Sigmatic — shipped (attribution target)”

Runbook actions (sigmatic.comment/snooze/add_exception) and a transactional case-ref outbox write back to Sigmatic /api/external/* via an external:write suite-service token. Target: preserve the initiating human analyst as first-class attribution on Sigmatic’s audit trail (#141; today attributed to a machine account).

7. Agent operates a product via /mcp — shipped

Section titled “7. Agent operates a product via /mcp — shipped”

One /mcp endpoint (PAT or OIDC) exposes a 77-tool catalog (26 VF / 35 Sigmatic / 16 Warden). Execution is uniform: dispatchMcpCall (apps/identity/src/authority/mcp.ts:313-360) routes every tool — VectorFlow’s included — over POST {peer}/api/peer/v1/mcp (apps/identity/src/authority/peers.ts:678) with an actor-scoped product-peer token minted through the workload exchange (peers.ts:425-438 → apps/identity/src/exchange/runtime.ts:258); each product re-enters its own native RBAC behind that contract. No in-process VectorFlow tRPC path remains. Every call is audited (mcp.tool_called, mcp.ts:369). See ADR-006, ADR-008. Target: actor role projection must not drop Warden approver (#131).

8. Enrichment: Sigmatic ↔ Warden — pull-only (shipped)

Section titled “8. Enrichment: Sigmatic ↔ Warden — pull-only (shipped)”

An analyst viewing a Sigmatic alert pulls related Warden cases via GET /api/related-cases (actor-scoped warden:cases:read). The former push webhook (/api/enrich/alert-context + X-Warden-Enrich-Token/WARDEN_ENRICH_TOKEN) was removed and must not return (#138); ADR-004 integration D is superseded by this pull model.

9. Product release → production — one release train (shipped)

Section titled “9. Product release → production — one release train (shipped)”

A product’s release workflow on its vX.Y.Z tag publishes its GHCR images and chart (Sigmatic/Warden <product>-{frontend,backend}; VectorFlow vectorflow-server-suite, vectorflow-agent) and its GitHub release. Veltro picks it up at the next suite cut. Cuts happen only when release-suite.yml (ADR-016 §30-31, #1127) is dispatched from main: once per batch of merges and product releases, or right away for a security fix. A dispatch:

  1. Cuts. From the newest main commit verify passed, it takes the next version from the release tags, pins each product at its newest published release (or one held back with hold; nothing unpublished is ever pinned), sets VELTRO_VERSION to the release version, and pushes a release commit tagged vX.Y.Z. bump picks patch, minor or major. A merge or a product release never cuts on its own. A train that tagged and then failed is retried by dispatching again.
  2. Builds the candidate. It publishes the identity image (a byte copy of the verified sha-<commit> image) and the suite charts at the release version, gates the release commit’s pins with suite_manifest.py pins (embedded as pin_provenance, with the release commit as pin_change), and signs and attests one manifest.
  3. Qualifies. On an isolated DinD runner, it installs the candidate’s exact image digests with VectorFlow alone, Sigmatic alone, the pair and all three products. Service smoke checks identity’s ready/not-installed component states, each installed product’s readiness through the gateway and Sigmatic/ Warden collection reads using the existing SDK smoke helpers. Full non-Lake backup/restore and PostgreSQL row-count, migration-ledger and identity-evidence comparisons run for each selection. CI runs no browser or end-to-end suite. Browser journeys, including scripts/install-matrix.sh, run in WSL on Danny’s PC with Google Chrome per AGENTS.md.
  4. Publishes the immutable GitHub release after qualification and the CLI build succeed. Its qualification evidence names the manifest’s SHA-256. An unavailable or disabled homelab doesn’t block publication. veltro-cli goes to PyPI after that. The API SDKs ship with the identity release below.
  5. Promotes. Only when HOMELAB_AUTOPROMOTE_ENABLED is armed. Promotion downloads the published manifest and deploys those digests. Later promotion evidence stays in attempt-scoped Actions artifacts; immutable release assets aren’t amended. The install-id baseline remains the last published release that carries promotion evidence. After an intentional datastore reset, pass accept_install_id on each promotion dispatch while that older baseline differs, including retries. The existing recovery dispatch and its migration checks stay available.
  6. Main catches up. The release commit sits only on its tag, so the release owner opens a PR moving main to the published pins: python3 scripts/suite_manifest.py adopt-release X.Y.Z (after git fetch --tags) writes the pins the release’s manifest shipped into versions.env and the charts, and takes Chart.lock from the tag. The release commit’s own diff won’t do: it’s against its source’s pins, which needn’t be main’s. Main’s versions.env is always exactly one published release’s pins; verify’s suite_manifest.py released-pins fails otherwise (#1460). The release workflow can’t open that PR itself: a PR opened with its GITHUB_TOKEN starts no workflow runs, so the required checks would never report.

The next suite cut needs VectorFlow, Sigmatic and Warden releases that read SUITE_AUTHORITY_V2_JWKS_URL (#1692). Compose and the umbrella chart no longer hand products identity’s public keys any other way, so qualification fails with older product releases pinned.

Identity also has its own release line (ADR-018). Dispatching release-identity.yml from main with bump set to patch or minor takes the newest main commit verify passed and the veltro-identity:sha-<commit> digest verify published for it. It scans and signs that digest, copies the same manifest bytes to ghcr.io/veltrosecurity/identity at X.Y.Z and X.Y, tags the commit identity/vX.Y.Z and publishes a GitHub release with the SBOM. The image is never rebuilt, a moved sha- tag or an already-used version is refused, and the signature comes from release-identity.yml on main. The numbers carry on from the suite’s, so the first identity release is one bump above the newest vX.Y.Z tag. Identity releases are never marked latest, and the suite’s tooling only reads vX.Y.Z tags, so a suite cut, install.sh or the CLI never picks one up.

The API SDKs ship with the identity release (#1691). Its packages job takes each product’s newest published release, regenerates veltro-sdk and @veltrosecurity/sdk from their OpenAPI documents, stamps them with the identity version, tests and builds them. The GitHub release waits for that build. Once it’s published, veltro-sdk goes to PyPI from a self-hosted runner (environment pypi-sdk) and @veltrosecurity/sdk goes to npm from the one GitHub-hosted job (environment npm-publish). Both skip a version that’s already on the registry.

The PyPI trusted publisher for veltro-sdk and the npm trusted publisher for @veltrosecurity/sdk still point at release-suite.yml. Before the first identity release, Danny has to:

  • re-point both at release-identity.yml, with the same environments. That means removing the release-suite.yml publisher, not adding a second one next to it, so the suite workflow can’t upload an SDK anymore. Until then the identity release’s uploads get refused.
  • limit the pypi-sdk and npm-publish environments to deployments from main.
  • add a tag ruleset that blocks updating or deleting identity/v* tags. The publish job already refuses to publish if the tag no longer names the source commit, and the ruleset stops the tag from moving at all.

Full Organization→Team→Environment multi-tenancy (#57), product-grade onboarding/honest health (#99), and governed detection lifecycle (#58) are suite foundations the closed loop consumes — they do not block calling the current single-tenant small-SOC loop complete.

10. Portable closed loop vs live Pangolin edge — split evidence

Section titled “10. Portable closed loop vs live Pangolin edge — split evidence”

Two complementary surfaces prove the closed loop. They are not interchangeable:

Surface What it proves How Status
Portable (vendor-neutral fixture) Sigma + MISP convergence on one case, reversible CrowdSec containment, approval seam scripts/seed-demo.sh + e2e/golden-path.spec.ts over compose/misp-mock.yml + compose/crowdsec-mock.yml (13 seams). The CrowdSec seam runs crowdsec-real-temporary-block: Warden’s crowdsec.block/status/unblock run cscli over ssh on the fixture host, the read-only crowdsec pack (installed from Warden’s registry) holds the bouncer key, and the seed and E2E read the decision on the LAPI’s bouncer route Shipped evidence. Run by hand; CI e2e retired 2026-09-23.
Live edge (veltro-dev Pangolin/Traefik) The seeded generic VectorFlow file source → Sigmatic sink and ClientHost/ClientAddr contract are applied to the edge testbed; a short-TTL decision from Warden’s real-LAPI actions is enforced by the real Traefik bouncer, expires naturally, and can be reversed by exact ID scripts/seed-pangolin.sh + e2e/pangolin-edge.spec.ts (by hand) Unrun. Dropped from 1.0 on 2026-09-23 with its dispatch workflow; the live residual and #37 stay open as backlog.

Rules of evidence:

  • A green portable seam is not real-service evidence (#46 owns that gate).
  • A green Warden run or a decision against compose/crowdsec-mock.yml is not proof of live enforcement — the live path must observe the exact decision in the real CrowdSec LAPI and prove removal (the named silent fake on #624).
  • Do not rebuild or relabel the portable fixtures as the Pangolin edge path; seed-pangolin.sh deliberately leaves them alone.

Operator entry points for the live half: docs/pangolin-sigmatic-automation.md.