Vetrix Docs

Admin: Security Policy

This document covers instance-wide security settings available to Vetrix administrators.

Runtime settings

Key Default Description
security.secret_detection_mode warn block, warn, or off
security.block_on_critical false Block PR merge if any unresolved critical SAST finding
security.block_on_high false Block PR merge if any unresolved high SAST finding
security.require_signed_commits false Require GPG/SSH-signed commits on all repos
security.password_min_length 12 Minimum password length for local accounts
security.session_timeout_minutes 1440 Web session lifetime (24 hours default)

Updating a setting

curl -X PUT https://<vetrix-host>/api/v1/admin/settings/<key> \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '"<value>"'

All values are strings. Boolean settings accept "true" or "false".

Secret detection

Enable blocking mode to reject pushes containing detected secrets:

curl -X PUT https://<vetrix-host>/api/v1/admin/settings/security.secret_detection_mode \
  -H "Authorization: Bearer <admin-token>" \
  -d '"block"'

See ../../system-docs/security/secrets.md for the full list of detected patterns and remediation guidance.

SAST merge gates

Block merge requests from merging when there are unresolved high-severity SAST findings:

curl -X PUT https://<vetrix-host>/api/v1/admin/settings/security.block_on_critical \
  -H "Authorization: Bearer <admin-token>" \
  -d '"true"'

curl -X PUT https://<vetrix-host>/api/v1/admin/settings/security.block_on_high \
  -H "Authorization: Bearer <admin-token>" \
  -d '"true"'

Individual repository maintainers can override these gates on a per-PR basis; overrides are recorded in the audit log.

Required-pipeline branch gate

Block merges, MR-opens, and pushes to a protected branch until the relevant commit has a passing CI pipeline. Unlike the SAST merge gates above (instance settings), this gate is configured per protected branch with the require_pipeline toggle, by a repository admin:

curl -X PUT https://<vetrix-host>/api/v1/repos/<owner>/<repo>/protected-branches/main \
  -H "Authorization: Bearer <repo-admin-token>" -H "Content-Type: application/json" \
  -d '{"restrict_push": false, "required_reviews": 0, "require_pipeline": true}'

The {pattern} path segment (here main) may contain slashes directly (.../protected-branches/release/*) or percent-encoded (release%2F*).

Behavior once enabled (full author-facing detail in user-docs/repositories/protected-branches.md):

  • Merge — allowed only when the commit's pipeline verdict is success or soft_only (all failures tolerated via allow_failure); hard_failure, pending, and missing are refused with 409 Conflict.
  • MR-open — refused only on a confirmed hard_failure; pending / missing are allowed so an MR can be opened while CI runs.
  • Push — rejected at the git protocol only when the pushed tip already has a hard_failure verdict; fails open on missing / pending (a push usually triggers the pipeline), and repo admins / push-allowlist members bypass it.

require_pipeline is independent of restrict_push. Enabling the pipeline gate does not by itself restrict normal direct pushes — set restrict_push: true separately for that.

Every gate decision is recorded in the audit log (repo.protected_branch.updated on config change; repo.merge.pipeline_blocked, repo.mr.pipeline_blocked, repo.push.pipeline_gate_blocked on a block; pipeline.job.hard_failed / pipeline.job.soft_failed on the underlying job outcomes). See Audit Log → Recorded actions.

OAuth2 configuration

OAuth2 external providers (GitHub, Google, GitLab, Microsoft Entra) are configured through admin settings — not environment variables. Client secrets are stored in app_settings under the oauth.<provider>.client_secret key and AES-256-GCM sealed with SECRET_ENC_KEY.

Set each value through the Admin UI (Admin → Settings → OAuth Providers) or the settings API:

curl -X PUT https://<vetrix-host>/api/v1/admin/settings/oauth.github.client_id \
  -H "Authorization: Bearer <admin-token>" -H "Content-Type: application/json" \
  -d '"<client-id>"'
curl -X PUT https://<vetrix-host>/api/v1/admin/settings/oauth.github.client_secret \
  -H "Authorization: Bearer <admin-token>" -H "Content-Type: application/json" \
  -d '"<client-secret>"'
curl -X PUT https://<vetrix-host>/api/v1/admin/settings/oauth.github.enabled \
  -H "Authorization: Bearer <admin-token>" -H "Content-Type: application/json" \
  -d '"true"'

Note: OAuth2 credentials cannot be set through environment variables — the server has no code path that loads OAuth2 credentials from the environment. The admin-settings surface above is the only supported configuration path. No server restart is needed — changes take effect on the next /api/v1/auth/oauth2/<provider>/start request.

See ../../user-docs/auth/oauth2-external-providers.md for the full per-provider walkthrough.

JWT security

JWTs are signed with an HMAC-SHA256 key set via JWT_SECRET (environment variable). Rotate by:

  1. Setting a new JWT_SECRET
  2. Restarting the server

All existing sessions are immediately invalidated. Users must log in again. Long-lived personal access tokens are not affected (they are stored separately, hashed with SHA-256).

SECRET_ENC_KEY (encryption at rest)

SECRET_ENC_KEY is a 64-character hex string (32 bytes, AES-256) that seals the following data at rest:

  • Webhook signing secrets (webhooks.secret_enc)
  • Encrypted backup archive credentials (backup_configs where encryption_enabled = true)
  • The SMTP password (app_settings key smtp.password)
  • OAuth client secrets (app_settings keys matching oauth.*.client_secret)

Startup behaviour:

  • Set and valid — server boots, vetrix: SECRET_ENC_KEY loaded logs at INFO.
  • Unset with dependent rows present — boot refuses with a clear error naming the row counts; fix the env var or remove the rows.
  • Unset with a clean DB — boot succeeds with a single WARN line; new webhook/backup/admin-secret writes keep failing until the key is configured.
  • Malformed (wrong length / bad hex) — boot refuses; fix the env var.

Manual key rotation

There is no in-process automated rotation. The procedure is:

  1. Export the webhooks, backup_configs, and relevant app_settings rows and decrypt them with the old key.
  2. Re-encrypt each value with the new key and write it back.
  3. Record an admin.secret_enc_key_rotated audit log entry (see POST /api/v1/admin/audit/events) citing the operator, timestamp, and affected row counts.
  4. Replace SECRET_ENC_KEY in the environment and restart the server.

If any step cannot be completed cleanly, do not deploy the new key — a half-rotated database leaves an unreachable set of ciphertexts.

Audit log

All privileged operations are written to the audit_log table and accessible via:

curl "https://<vetrix-host>/api/v1/admin/audit?page=1&per_page=50" \
  -H "Authorization: Bearer <admin-token>"

Audit events include: user creation/deletion, password changes, impersonation sessions, runner registration/deletion, settings changes, security gate overrides.

Two-factor authentication

Instance-wide 2FA enforcement:

# Require all users to enroll 2FA within 7 days
curl -X PUT https://<vetrix-host>/api/v1/admin/settings/security.require_2fa \
  -H "Authorization: Bearer <admin-token>" \
  -d '"true"'

Users who have not enrolled are redirected to the 2FA setup page on login until they complete enrollment.

IP allowlisting

Restrict API access to specific IP ranges via the security.ip_allowlist setting. Accepts a comma-separated list of CIDR blocks:

curl -X PUT https://<vetrix-host>/api/v1/admin/settings/security.ip_allowlist \
  -H "Authorization: Bearer <admin-token>" \
  -d '"10.0.0.0/8,192.168.1.0/24"'

An empty value (default) allows all IPs. Admin endpoints (/api/v1/admin/*) are always accessible from localhost regardless of this setting.

Maintenance mode

Enable maintenance mode to take the instance offline for all non-admin users:

curl -X PUT https://<vetrix-host>/api/v1/admin/settings/maintenance.enabled \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '"true"'

Admin users (JWT claims is_admin=true) and admin API endpoints (/api/v1/admin/*) remain accessible during maintenance mode.