Skip to content

Security

Every route in VectorStep’s HTTP API and UI requires a credential. This page covers the token and role model, logging into the UI, what allow_unauthenticated means and when it’s acceptable, and the audit log.

Authentication is named tokens in config, not a user system — no accounts, no passwords, no invitations, no SSO, no per-object permissions:

auth:
tokens:
- name: platform-admin
token: ${VS_TOKEN_PLATFORM_ADMIN}
role: admin
- name: sre-oncall
token: ${VS_TOKEN_SRE}
role: operator
- name: dashboards
token: ${VS_TOKEN_VIEWER}
role: viewer
- name: payments-alerts
token: ${VS_TOKEN_PAYMENTS}
role: webhook
team: payments

Each entry has a name (unique, for audit attribution), a token (a plain shared secret — generate one with openssl rand -hex 24, resolved from an environment variable via ${ENV_VAR} as shown, or written directly if you’re not using env vars for secrets), and a role. team is valid only on a webhook-role token — see Team attribution.

There are four roles:

Role Grants
admin Everything — pipeline/step config writes, /reload, and everything operator and viewer can do. Config write access is equivalent to shell access on the host once security.allow_shell_checks is enabled, so treat an admin token accordingly.
operator Trigger/rerun/replay a pipeline, submit feedback, decide approvals. Everything viewer can do, but no config writes.
viewer Read-only — every GET route and UI page.
webhook POST /webhook only. Outside the admin/operator/viewer ranking entirely — an admin or operator token cannot be used as a webhook credential, and a webhook token cannot reach anything else. This keeps webhook attribution meaningful: whoever holds a webhook token really is only a machine ingesting alerts, not an operator who happens to reuse the same credential.

admin implies operator implies viewer — a higher role can always do what a lower one can.

Routes Required role
GET /health, GET /favicon.svg none
GET /metrics none by default; viewer when auth.metrics_auth: true
POST /webhook webhook
Every other GET route, and every GET /ui/* page (including the live run log stream) viewer
POST /pipelines/{name}/run, POST /runs/{id}/rerun, replay launches, feedback submissions, golden-set edits, approval decisions operator
Pipeline/step create, update, delete; both /validate routes; POST /reload admin

POST /pipelines/{name}/promotion-readiness/preview is viewer — it computes and returns a candidate readiness evaluation with no side effects, so it doesn’t need a write-capable credential even though it’s a POST.

GET /ui/login renders a token field. Submitting a valid token sets a signed session cookie (vectorstep_session, HttpOnly, SameSite=Lax) and redirects to wherever you were headed. The cookie carries the token’s name, role, and team, and is re-validated against the current auth.tokens config on every request — removing a token from config and restarting the service revokes any session minted from it immediately, with no separate logout step needed on your end.

An unauthenticated browser request to a /ui/* page redirects to the login page; an unauthenticated API request gets a 401 with a JSON body instead. The one exception is the live run log stream (GET /ui/runs/{id}/stream), which always 401s rather than redirecting — a redirect response doesn’t make sense for a browser EventSource connection.

Every POST form under /ui/* carries a CSRF token derived from your session, checked before the action runs. This is separate from the role check above — both have to pass.

Sessions last 8 hours by default (auth.session_max_age_seconds), and rely on auth.session_secret to survive a service restart — without it, a fresh random secret is generated at every startup and every existing session is invalidated when the service restarts. Checking “Remember me” at login mints a longer session instead — auth.session_max_age_remembered_seconds, default 30 days — see Logging in.

VectorStep refuses to start with no auth.tokens configured, because pipeline write access is equivalent to shell access on the host once security.allow_shell_checks is enabled (see Configuration reference). Set auth.allow_unauthenticated: true to opt out of this check and run with the entire HTTP API and UI open to anyone who can reach the port:

auth:
allow_unauthenticated: true

This is only appropriate on a trusted local machine — a laptop, an isolated dev sandbox — never on anything reachable from a shared network. The service logs a warning on every startup while this is set, deliberately not just once, so it stays visible in every log sample an operator ever looks at.

Authentication is named tokens, not accounts — there’s no self-service way for a team to request or manage its own credentials in-product. If a platform team runs VectorStep for several other teams, see Multi-team config ownership for how to structure that with a git repo, CODEOWNERS, and a request process, without building anything new.

Every config-writing route, /reload, and every approval decision (approve or reject) writes a row recording who did it: the token’s name, its role, what was changed, and — for a pipeline or step write — the prior YAML content, so a mistaken edit is recoverable without digging through git history for a directory that (for the step library) isn’t even version-controlled. Approvals decided through the UI record the session’s token name; approvals decided via a Telegram or Slack button record that platform’s own user id (telegram:<id> / slack:<id>) rather than a VectorStep identity, since there’s no mapping between a chat account and a token.

No row ever contains a token value, a token prefix, or a request body that might contain one — only names and content. GET /audit (viewer-role) returns a filterable page of rows; the run detail page in the UI surfaces the approval decision for that run directly, answering “who approved this” without a separate lookup.

vectorstep_auth_failures_total{reason} counts authentication/authorization failures by reason (missing, invalid, forbidden) — watch this for credential-stuffing attempts against the service, alongside the existing Prometheus surface at GET /metrics (see Observability).