Skip to content

ADR-003: Veltro as a standalone app + the suite identity authority

Source of truth: docs/adr/003-veltro-standalone-identity-authority.md (synced at build time — edit the original, not this copy). Status: Accepted (2026-07-03). Owner confirmed Option A (Decision 3): Veltro is a deployment role of the shared platform codebase. Supersedes ADR-001 Decision 2 (VectorFlow as the identity authority); keeps ADR-001 Decision 1 (separate repos + thin platform repo) and the ADR-002 module contract, amended so the delegated issuer is Veltro, not VectorFlow.

ADR-010 partial supersession (2026-07-30): Veltro remains the suite identity authority, but Decision 3’s shared VectorFlow source, schema, database, image, version, and VELTRO_ROLE=identity realization is historical implementation rather than the approved target. #202 extracts an independent lightweight application fixed to public realm default.

Amended by ADR-004 (2026-07-04): Decision 4’s three *_DELEGATED_AUTH flags are consolidated into one VELTRO_INTEGRATED per product, and VF_SESSION_SECRET is renamed VELTRO_SESSION_SECRET. The identity mechanism (cookie, VELTRO_ROLE, DB-sharing) is unchanged.

Today “Veltro” is not an application — it is:

  • this platform repo (compose profiles, single-origin nginx, env, ADRs), and
  • an nginx alias /veltro/* → vectorflow:3000/vectorflow/veltro/*, i.e. the VectorFlow container serves the identity surface.

Recent upstream work sharpened the seams:

  • VF #529 consolidated identity/org/suite-audit settings under a cohesive src/app/(dashboard)/veltro/settings/* subtree (SSO, SCIM, users, roles, teams, organization, suite-audit), retitled VF’s own hub “VectorFlow Settings”, and made every Veltro entry point a clean-origin <a href="/veltro/…">.
  • CHAD #281 added suiteOwned handoff (identity tiles hidden in delegated mode) + VELTRO_BASE=/veltro; #282 (this effort) matched CHAD’s settings hub to VF 1:1.
  • The design system ships on npm (@veltrosecurity/{tokens@0.1.1, ui@0.1.1,shell@0.2.1}), already consumed by both apps — the shared-UI half of “suite layer” is solved and repo-independent.

What remains is the inversion the goal asks for: Veltro must become its own deployable application (own image, container, compose, database) that IS the suite identity/SSO authority, with VectorFlow and CHAD reduced to delegating peers — while each product still runs fully standalone with its own auth when deployed on its own compose.

ADR-001 deliberately made VF the authority and rejected “a fourth stateful service.” This ADR accepts that operational cost on purpose: a suite that adds Warden/Prowl/… needs identity to live in a layer no single product owns (the Zscaler ZIdentity model). Naming the tradeoff once: +1 stateful service (Veltro app + its Postgres) in the full deployment.

Decision 1 — Veltro is its own app and the suite identity authority

Section titled “Decision 1 — Veltro is its own app and the suite identity authority”

Introduce a Veltro application: own ghcr image (ghcr.io/veltrosecurity/veltro-*), own container, own Postgres, own compose profile. Veltro owns, for the suite:

  • Login (credentials + TOTP, WebAuthn, OIDC, SAML) — the suite sign-in.
  • Identity settings — SSO/OIDC, SAML, SCIM, users, roles, teams.
  • The launcher (/) + the AppSwitcher app registry (already in @veltrosecurity/shell).
  • Shared suite services — the cross-app audit feed, and the S2S token plane that fans out to products.

Under Veltro’s compose, VF and CHAD are delegators: they trust Veltro’s session and redirect sign-in / identity-management to Veltro. On their own composes they are unchanged — own login, own SSO/SCIM/local users.

Decision 2 — Session mechanism is unchanged; only the issuer moves

Section titled “Decision 2 — Session mechanism is unchanged; only the issuer moves”

Keep ADR-001’s proven mechanism: a single-origin, host-only Auth.js v5 JWE session cookie, single-tenant (org_id="default"), signed/encrypted with the shared NEXTAUTH_SECRET (HKDF-SHA256 → dir/A256CBC-HS512). The only change is who mints it: Veltro, not VF.

  • CHAD already decodes it generically (backend/app/core/vf_session.py does not check iss). Repoint by pointing VF_SESSION_SECRET at Veltro’s secret — config-only. (A follow-up may rename the env to VELTRO_SESSION_SECRET with a back-compat alias.)
  • VF in delegated mode reads the cookie natively: VF is Auth.js with the same shared secret in single-tenant mode, so a Veltro-minted cookie is already a valid VF session (getToken with NEXTAUTH_SECRET). VF delegated mode is therefore mostly gating, not new crypto (see Decision 4).

This is why the mechanism is low-risk: the cookie bytes are identical to today; only the process that sets them changes.

Decision 3 — Realization: Veltro is a deployment role of the shared platform codebase

Section titled “Decision 3 — Realization: Veltro is a deployment role of the shared platform codebase”

Decided — Option A (owner-confirmed 2026-07-03). Build Veltro from the shared platform codebase in an identity/authority role rather than as a clean-room rewrite. This makes Veltro a separate deployable app (own image/container/DB/ compose/authority), while sharing source with VF; it satisfies “its own app” at the deployment boundary, not as a separate codebase (owner accepted this tradeoff):

  • The identity stack (auth.ts, saml, scim, webauthn, jwt-key, the veltro/settings/* pages, the launcher, @veltrosecurity/shell) already exists and is verified. Veltro runs that stack; VF runs the same source in its product role.
  • A build/run flag (VELTRO_ROLE=identity) makes the app serve only the identity surface (/, /login, /veltro/**, /api/auth/**, /api/scim/**, /api/auth/saml/**, launcher) and hide product features (pipelines/fleet/dashboards). Ship as image veltro-server.
  • Because Veltro and VF share the identity code and secret, the cookie contract is identical by construction — CHAD/VF decode unchanged.

Rationale: maximal reuse of proven, verified auth + crypto; identical cookie; fully buildable/verifiable here (a role flag, not a new IdP). “Separate app” is satisfied at the deployment boundary (own image/container/DB/compose/authority), the way a monorepo builds multiple apps from shared packages.

Alternative (Option C), rejected for now. A brand-new clean-room Veltro app (own codebase + minimal single-tenant schema) reusing only the npm design system

  • cookie format. Truest “separate codebase,” but a large net-new IdP whose OIDC/SAML/SCIM/TOTP correctness cannot be verified without a live stack + real IdPs — high risk of shipping unverified auth. Resumable later by extracting the identity stack into an @veltrosecurity/identity package consumed by a thin Veltro app (and VF), which is the clean long-term end state.

Owner decision (2026-07-03): Option A. Only the Veltro app’s internals differ between A and C; everything else in this ADR is identical either way.

DB-sharing corollary (Option A). Veltro and the suite VectorFlow are the same app + Prisma schema, so in the full profile they share ONE database (vf-postgres) — Veltro migrates it first, and a user Veltro provisions is visible to the VF product app (no cross-DB JIT needed for VF). CHAD keeps its own DB and JIT-provisions from the cookie claims (backend/app/services/ suite_auth.py). A FUTURE product joining the suite for identity must therefore either (a) share this VF-schema identity DB, or (b) JIT-provision from the session claims like CHAD — prefer (b) when it has its own schema.

Decision 4 — VF and CHAD standalone integrity is inviolable

Section titled “Decision 4 — VF and CHAD standalone integrity is inviolable”
  • Own compose = unchanged. vectorflow/docker/server/docker-compose.yml and chad/docker-compose.yml keep their own login/SSO/SCIM/local users, no Veltro dependency. The delegated flags default off (CHAD_DELEGATED_AUTH=false; a new VF_DELEGATED_AUTH=false).
  • VF delegated mode (new, suite only). When VF_DELEGATED_AUTH=true: VF stops offering its own sign-in (redirect /login → Veltro), redirects /veltro/** identity management to Veltro, and trusts the incoming Veltro-minted session (native, shared secret). VF keeps its own encryption-at- rest key (VF_ENCRYPTION_KEY_V2) for its instance secrets — that is a VF concern, independent of who mints sessions.
  • Secret ownership. NEXTAUTH_SECRET becomes Veltro’s crown-jewel (mints suite sessions + signs S2S tokens). Compose fans it to VF and CHAD as the session-verification secret. Rotating it logs everyone out of all three.

veltro-nginx, single origin:

  • / → Veltro launcher (was 302 /vectorflow/launcher).
  • /login, /veltro/**, /api/auth/**, /api/scim/** → Veltro.
  • /vectorflow/** → VectorFlow (delegator; basePath image).
  • /chad/** → CHAD (delegator). Cookie stays host-only on the single origin (unchanged requirement).
  • +1 stateful service: the Veltro app container (the vectorflow-server image in identity role). It SHARES vf-postgres with the suite VectorFlow (Decision 3 corollary) — no separate identity DB, and versions.env needs no new pin (Veltro rides VF_VERSION).
  • Veltro (identity role) SKIPS VF’s product singleton schedulers (delivery retry, auto-rollback, staged rollout, fleet/anomaly/cost/metrics, git-sync, backups): they must run on exactly one writer against the shared DB, so the co-located product instance runs them (src/instrumentation.node.ts gates on isVeltroIdentityRole(); verified — the identity container starts none of them). Edge: a future Veltro-only deploy (no VF product sibling) would not run auth-challenge-gc; split identity vs product singletons if that shape ships.
  • ADR-002 module contract amended: the delegated issuer is Veltro; new modules point *_DELEGATED_AUTH + session secret at Veltro. Everything else stands.
  • Because Veltro shares vf-postgres, existing VF-as-authority suite deploys need NO user/org migration — Veltro reads the same identity tables. (A later move to a dedicated Veltro DB would need a migration; not required for Option A.)
  • Standalone VF/CHAD are untouched — their own compose profiles exclude Veltro and the delegated flags default off (verified via docker compose config).
  1. P1 (done): CHAD settings hub visual parity (#282).
  2. P2: This ADR + owner confirmation of Decision 3 (A vs C).
  3. P3: Veltro app — identity role (Option A): VELTRO_ROLE gating, image, launcher/login/identity at / + /veltro. Build + unit/route tests.
  4. P4: VF delegated mode (VF_DELEGATED_AUTH) — login/identity redirect + native session trust; standalone unchanged. Unit tests both modes.
  5. P5: Repoint CHAD delegation VF→Veltro (config; optional env rename).
  6. P6: Veltro compose profile + veltro-postgres + nginx re-topology + .env/versions.env contract.
  7. P7: Verification — extend scripts/verify-sso.sh for the Veltro issuer; confirm VF-only and CHAD-only composes still boot standalone; update README/ADR-001/ADR-002 cross-refs + a migration runbook.

Acceptance mirrors scripts/verify-sso.sh: / routing, Veltro login → /vectorflow and /chad/ authed with no second login, single logout, and both products still boot standalone with their own login.