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 snapshotdocs/discovery/GOLDEN-PATHS.md. Terms:CONTEXT.md. Authority: the ADRs indocs/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:
- Cuts. From the newest
maincommit verify passed, it takes the next version from the release tags, pins each product at its newest published release (or one held back withhold; nothing unpublished is ever pinned), setsVELTRO_VERSIONto the release version, and pushes a release commit taggedvX.Y.Z.bumppicks 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. - 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 withsuite_manifest.py pins(embedded aspin_provenance, with the release commit aspin_change), and signs and attests one manifest. - 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 perAGENTS.md. - 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-cligoes to PyPI after that. The API SDKs ship with the identity release below. - Promotes. Only when
HOMELAB_AUTOPROMOTE_ENABLEDis 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, passaccept_install_idon each promotion dispatch while that older baseline differs, including retries. The existing recovery dispatch and its migration checks stay available. - 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(aftergit fetch --tags) writes the pins the release’s manifest shipped intoversions.envand the charts, and takesChart.lockfrom 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’sversions.envis always exactly one published release’s pins; verify’ssuite_manifest.py released-pinsfails otherwise (#1460). The release workflow can’t open that PR itself: a PR opened with itsGITHUB_TOKENstarts 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 therelease-suite.ymlpublisher, 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-sdkandnpm-publishenvironments to deployments frommain. - add a tag ruleset that blocks updating or deleting
identity/v*tags. Thepublishjob already refuses to publish if the tag no longer names the source commit, and the ruleset stops the tag from moving at all.
Not gating the small-SOC product claim
Section titled “Not gating the small-SOC product claim”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.ymlis 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.shdeliberately leaves them alone.
Operator entry points for the live half: docs/pangolin-sigmatic-automation.md.