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
successorsoft_only(all failures tolerated viaallow_failure);hard_failure,pending, andmissingare refused with409 Conflict. - MR-open — refused only on a confirmed
hard_failure;pending/missingare 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_failureverdict; fails open onmissing/pending(a push usually triggers the pipeline), and repo admins / push-allowlist members bypass it.
require_pipelineis independent ofrestrict_push. Enabling the pipeline gate does not by itself restrict normal direct pushes — setrestrict_push: trueseparately 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>/startrequest.
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:
- Setting a new
JWT_SECRET - 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_configswhereencryption_enabled = true) - The SMTP password (
app_settingskeysmtp.password) - OAuth client secrets (
app_settingskeys matchingoauth.*.client_secret)
Startup behaviour:
- Set and valid — server boots,
vetrix: SECRET_ENC_KEY loadedlogs 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:
- Export the
webhooks,backup_configs, and relevantapp_settingsrows and decrypt them with the old key. - Re-encrypt each value with the new key and write it back.
- Record an
admin.secret_enc_key_rotatedaudit log entry (seePOST /api/v1/admin/audit/events) citing the operator, timestamp, and affected row counts. - Replace
SECRET_ENC_KEYin 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.