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.
Tokens and roles
Section titled “Tokens and roles”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: paymentsEach 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.
Route requirements
Section titled “Route requirements”| 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.
Logging into the UI
Section titled “Logging into the UI”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.
allow_unauthenticated
Section titled “allow_unauthenticated”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: trueThis 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.
Multiple teams, one instance
Section titled “Multiple teams, one instance”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.
Audit log
Section titled “Audit log”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.
Metrics
Section titled “Metrics”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).