Repository navigation
API Reference
TL;DR
- What: 50+ REST API endpoints covering auth, user management, server operations, terminal sessions, audit logs, notifications, LDAP config, SMTP config, hub settings, and agent/CLI installation
- Who: Frontend developers, integration builders, and anyone automating Veyport
- Why: Complete reference for every HTTP endpoint with request/response schemas
- Where: All endpoints served by the Hub on the HTTP port (default :8081)
- When: After authentication - most endpoints require a valid JWT access token or CLI-created API token
- How: JSON request/response bodies; secure cookie auth for browsers with Bearer fallback for scripts/API tokens; SSE for streaming
- Overview
- Authentication Flow
- Error Format
- Auth Endpoints
- User Management Endpoints
- Audit Log Endpoints
- Server Endpoints
- Path Permission Endpoints
- Agent Operation Endpoints
- Installation Endpoints
- Server Removal Endpoints
- Hub Settings Endpoints
- LDAP Directory Endpoints
- SMTP and Notification Endpoints
- Re-Enrollment Endpoints
- Terminal Endpoints
- SSH Gateway Endpoints
- Session Endpoints
- Application Access Endpoints
| Property | Value |
|---|---|
| Base URL |
http://<hub-host>:8081 (dev) or https://<hub-domain> (prod) |
| Content Type |
application/json (request and response) |
| Browser Auth |
__Host-veyport_access and __Host-veyport_refresh secure cookies, plus __Host-veyport_csrf for mutating requests |
| API Client Auth | Authorization: Bearer <JWT access token or CLI-created API token> |
| Pagination |
?limit=N&offset=N (default limit varies, max 100) |
| CORS | Enabled in dev mode for http://localhost:5173
|
| Caching |
Cache-Control: no-store on all API responses |
All request and response bodies are JSON unless otherwise noted (file uploads use multipart/form-data, SSE streams use text/event-stream, binary downloads use application/octet-stream).
For browser sessions, successful login and refresh flows set auth cookies and may omit token strings from the JSON response body. The cURL examples below use Bearer headers because they are intended for scripts and API clients.
Veyport uses five JWT token types. In browser-driven flows, the resulting access and refresh tokens are set as secure cookies.
| Token Type | Purpose | Lifetime |
|---|---|---|
| Setup | Issued after first login when TOTP is not yet configured. Authorizes /api/auth/totp/setup and /api/auth/totp/enable. |
Short-lived |
| TOTP | Issued after password verification when TOTP is enabled. Authorizes /api/auth/login/totp. |
Short-lived |
| Password recovery | Issued after existing MFA for a temporary-password account. Authorizes only /api/auth/password/recovery; never becomes a normal session. |
Short-lived, capped by temporary-password expiry |
| Access | General-purpose API token. Required by most endpoints. | Short-lived |
| Refresh | Long-lived token used to obtain a new access/refresh pair via /api/auth/refresh. |
Long-lived |
For non-browser automation, Veyport also supports opaque API tokens created with the Hub CLI. These are not JWTs, are stored as SHA-256 hashes at rest, and are recommended over automating a human password + TOTP secret.
Create a dedicated low-privilege user for automation, then mint an API token on the Hub host:
# Docker deployment
docker exec veyport /app/veyport admin create-api-token \
--username scanner \
--name nightly-scan \
--expires-in 720h \
--db /data/veyport.db
# Bare-metal deployment
./bin/veyport admin create-api-token \
--username scanner \
--name nightly-scan \
--expires-in 720h \
--db /var/lib/veyport/veyport.dbUse the returned token with the standard Bearer header:
curl -H "Authorization: Bearer adt_..." https://hub.example.com/api/auth/meAPI tokens can call normal access-token-protected API endpoints, but they are intentionally rejected on interactive account-management endpoints such as password changes, avatar updates, and TOTP disable.
While a temporary password requires replacement, requests for normal access return a
machine-readable password_change_required error. This does not grant partial CLI, API, SSH or
application access: users with existing TOTP receive only a purpose-bound recovery token after
TOTP verification, and that token accepts only the browser password-recovery operation. See
Logging In and CLI for the recovery steps.

All errors return a JSON object with a single error key:
{
"error": "human-readable error message"
}The HTTP status code indicates the error category:
| Status | Meaning |
|---|---|
| 400 | Bad Request - invalid input, missing required fields |
| 401 | Unauthorized - invalid credentials or expired token |
| 403 | Forbidden - insufficient permissions |
| 404 | Not Found - resource does not exist |
| 409 | Conflict - resource already exists (e.g., duplicate user) |
| 413 | Request Entity Too Large - file exceeds size limit |
| 429 | Too Many Requests - rate limit exceeded |
| 500 | Internal Server Error |
| 502 | Bad Gateway - agent communication failure |
| 504 | Gateway Timeout - agent did not respond in time |
Source: hub/internal/server/respond.go -- respondError(w, status, message) produces {"error": message}.
Check whether the Hub has been initialized (at least one user exists).
| Property | Value |
|---|---|
| Auth | None |
| Rate Limited | No |
Response (200) - unauthenticated:
{
"initialized": true
}Response (200) - authenticated (with valid access token):
{
"initialized": true,
"version": "vX.Y.Z"
}Note: The
versionfield is only included when the request carries a valid access token. Unauthenticated requests receive only theinitializedfield.
cURL:
curl https://hub.example.com/api/auth/statusRegister the first admin user. Only works when no users exist (initialized: false).
| Property | Value |
|---|---|
| Auth | None |
| Rate Limited | Yes (10 requests / 60s) |
Request Body:
{
"username": "admin",
"email": "admin@example.com",
"password": "S3cur3P@ssw0rd!"
}Validation rules:
- Username: 3-32 characters, alphanumeric and underscores only
- Password: must satisfy
auth.ValidatePasswordPolicy(minimum length, complexity)
Response (200):
{
"setup_token": "eyJhbGci...",
"user": {
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"totp_enabled": false,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}Error Cases:
-
403- Registration disabled (users already exist) -
400- Invalid username or password policy violation -
409- User already exists
cURL:
curl - X POST https://hub.example.com/api/auth/register \
- H 'Content-Type: application/json' \
- d '{"username":"admin","email":"admin@example.com","password":"S3cur3P@ssw0rd!"}'Authenticate with username and password.
| Property | Value |
|---|---|
| Auth | None |
| Rate Limited | Yes (10 requests / 60s) |
Request Body:
{
"username": "admin",
"password": "S3cur3P@ssw0rd!"
}Response when TOTP is not set up (200):
{
"setup_token": "eyJhbGci...",
"requires_totp_setup": true
}Response when TOTP is enabled (202):
{
"totp_token": "eyJhbGci..."
}Error Cases:
-
401- Invalid credentials -
403- Account disabled or dormant:{"error":"account disabled — contact an administrator"}/{"error":"account dormant — contact an administrator"}. Returned before the password is checked or any LDAP bind is attempted; see Account State Enforcement. -
423- Account temporarily locked:{"error":"account temporarily locked — try again later"}. Returned before the password is checked or any LDAP bind is attempted, and does not change the account's failure count.
cURL:
curl - X POST https://hub.example.com/api/auth/login \
- H 'Content-Type: application/json' \
- d '{"username":"admin","password":"S3cur3P@ssw0rd!"}'Complete login by providing a TOTP code (second factor).
| Property | Value |
|---|---|
| Auth | None (TOTP token in body) |
| Rate Limited | Yes (3 requests / 60s) |
Request Body:
{
"totp_token": "eyJhbGci...",
"code": "123456"
}Response (200):
{
"user": {
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"totp_enabled": true,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}On success, the Hub also sets __Host-veyport_access, __Host-veyport_refresh, and __Host-veyport_csrf cookies for browser clients.
For a user with an existing TOTP secret whose temporary password still requires replacement,
this endpoint instead returns { "must_change_password": true, "recovery_token": "...", "expires_at": "..." }. It sets no normal access or refresh cookies and returns no user profile.
The purpose-bound recovery token permits only password replacement. After TOTP verification, the
user completes the change in the browser; the vey CLI cannot use recovery authority to access
commands. After replacement, the user signs in again with the new password and existing TOTP.
Accounts without TOTP retain the existing TOTP enrollment flow.
Error Cases:
-
401- Invalid or expired TOTP token, invalid TOTP code, user not found -
403- Account disabled or dormant, same messages as login, returned before the code is validated; see Account State Enforcement. -
423- Account temporarily locked:{"error":"account temporarily locked — try again later"}. Returned before the code is validated. A wrong code counts as a failure exactly like a wrong password; a correct code resets the failure count and clears the lock.
cURL:
curl - X POST https://hub.example.com/api/auth/login/totp \
- H 'Content-Type: application/json' \
- d '{"totp_token":"eyJhbGci...","code":"123456"}'Exchange a refresh token for a new access/refresh token pair.
| Property | Value |
|---|---|
| Auth | None. Browser clients usually send the __Host-veyport_refresh cookie; non-browser clients may send refresh_token in the JSON body |
| Rate Limited | Yes (30 requests / 60s) |
Request Body:
{
"refresh_token": "eyJhbGci..."
}Response (200):
{}On success, the Hub atomically rotates the refresh token for the session making this request and
sets new __Host-veyport_access, __Host-veyport_refresh, and __Host-veyport_csrf cookies. Rotation does not
invalidate independent sessions or access tokens that remain valid. Explicit logout, expiry,
account or security changes, and session revocation retain their existing effects.
Error Cases:
-
401- Invalid or expired refresh token -
401- Account disabled or dormant, same messages as login; see Account State Enforcement
cURL:
curl - X POST https://hub.example.com/api/auth/refresh \
- H 'Content-Type: application/json' \
- d '{"refresh_token":"eyJhbGci..."}'Generate a new TOTP secret and QR URL. The secret is stored but not yet enabled.
| Property | Value |
|---|---|
| Auth | Setup token (Bearer) |
| Rate Limited | No |
Request Body: None
Response (200):
{
"secret": "JBSWY3DPEHPK3PXP",
"qr_url": "otpauth://totp/Veyport:admin?secret=JBSWY3DPEHPK3PXP&issuer=Veyport"
}Error Cases:
-
401- Invalid or expired setup token -
404- User not found
cURL:
curl - X POST https://hub.example.com/api/auth/totp/setup \
- H 'Authorization: Bearer <setup_token>'Verify and enable TOTP by providing a valid code. For users created with a temporary password, this is also where the permanent password is set.
Password replacement, history, factor enrollment, recovery-grant consumption, setup state and success audits commit atomically. A persistence failure preserves the original setup state; retry with a fresh authenticator code.
| Property | Value |
|---|---|
| Auth | Setup token (Bearer) |
| Rate Limited | No |
Request Body:
{
"code": "123456",
"new_password": "OptionalUnlessTemporaryPassword"
}Response (200):
{
"user": {
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"totp_enabled": true,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}On success, the Hub also sets __Host-veyport_access, __Host-veyport_refresh, and __Host-veyport_csrf cookies for browser clients. This also counts as a successful sign-in: the account's failure count resets and last_login_at is stamped, same as a normal login.
Error Cases:
-
401- Invalid TOTP code -
400- TOTP not set up (call/api/auth/totp/setupfirst) -
409- Setup snapshot changed or TOTP already enabled; sign in again -
403- Recovery requires current governance or audited shell authority -
500- Authentication change could not be persisted; details are withheld
cURL:
curl - X POST https://hub.example.com/api/auth/totp/enable \
- H 'Authorization: Bearer <setup_token>' \
- H 'Content-Type: application/json' \
- d '{"code":"123456"}'Get the current authenticated user's profile.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
| Rate Limited | No |
Response (200):
{
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"totp_enabled": true,
"avatar": "data:image/png;base64,...",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"failed_login_count": 0,
"last_login_at": "2026-09-04T22:10:33Z",
"status": "active",
"dormancy_exempt": false,
"last_activity_at": "2026-09-04T22:10:33Z"
}Same fields (and same meaning) as GET /api/users above: failed_login_count,
last_failed_login_at, last_login_at, locked_until, status, disabled_at, disabled_by,
reactivated_at, dormancy_exempt, last_activity_at — always about the caller's own account.
Error Cases:
-
404- User not found -
401- Account disabled or dormant; see Account State Enforcement
cURL:
curl https://hub.example.com/api/auth/me \
- H 'Authorization: Bearer <access_token>'Change the authenticated user's password.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
| Rate Limited | No |
Request Body:
{
"current_password": "OldP@ssw0rd!",
"new_password": "N3wP@ssw0rd!"
}Response (200):
{
"status": "password updated"
}Note: A successful password change invalidates all of the user's existing sessions, including the requesting session, and revokes API tokens and existing SSH certificates. Sign in again; CLI users must also obtain a fresh certificate with
vey ssh-cert.
Error Cases:
-
401- Invalid current password -
400- New password does not meet policy requirements -
404- User not found
cURL:
curl - X PUT https://hub.example.com/api/auth/password \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"current_password":"OldP@ssw0rd!","new_password":"N3wP@ssw0rd!"}'Update the authenticated user's avatar image.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
| Rate Limited | No |
Request Body:
{
"avatar": "data:image/png;base64,iVBOR..."
}Send an empty string to remove the avatar.
Limits: Maximum ~500KB base64-encoded data URL (700,000 characters).
Response (200):
{
"status": "avatar updated"
}Error Cases:
-
400- Avatar image too large (max 500KB)
cURL:
curl - X PUT https://hub.example.com/api/auth/avatar \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"avatar":"data:image/png;base64,iVBOR..."}'Admin-only: disable TOTP for another user. Requires the admin's own TOTP code for verification.
Success confirms one committed transaction removing the factor, advancing token and SSH credential generations, revoking browser/API/application authority and recording the success audit. Previously issued SSH certificates remain revoked after MFA reenrollment. Protected principals require governed recovery.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
| Rate Limited | No |
Request Body:
{
"user_id": "target-user-uuid",
"admin_totp_code": "123456"
}Response (200):
{
"status": "totp disabled"
}Error Cases:
-
401- Invalid admin TOTP code -
404- Admin user not found -
403- Not an admin -
403- Target is a protected principal -
500- Persistence failed; factor and authority changes were rolled back
cURL:
curl - X POST https://hub.example.com/api/auth/totp/disable \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"user_id":"target-user-uuid","admin_totp_code":"123456"}'A disabled or dormant account (see the User Management section below) is refused everywhere it
could otherwise act, with a state-specific message that reveals nothing about credential
correctness: {"error":"account disabled — contact an administrator"} or
{"error":"account dormant — contact an administrator"}.
| Path | Response |
|---|---|
POST /api/auth/login (local and LDAP) |
403, before the password is checked or any LDAP bind is attempted; the account's failure count is unchanged; audited user.login_failed with detail account disabled/account dormant
|
POST /api/auth/login/totp |
403, same messages, before the code is validated |
POST /api/auth/refresh |
401, same messages |
| Any request bearing an access token or an API token |
401. For a dormant account the body carries the same message. For a disabled account the credentials themselves are already dead — disabling bumps the token generation and revokes every API token — so those requests get the generic invalid token. A browser client redirects to /login on the next request either way |
POST /api/ssh/certificates |
401, same messages (the access token is refused before the handler runs, like any other token-bearing request) |
| SSH gateway shell | refused with banner veyport: <same message>, audited ssh.session_refused
|
Precedence at sign-in is disabled > dormant > locked (423, see above) > credential check. A
locked account (007) is still refused only at the two sign-in stages above — it is not checked on
refresh, token-bearing requests, or the SSH endpoints.
All user management endpoints require admin role.
List all users.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"users": [
{
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"auth_provider": "local",
"terminal_access": false,
"totp_enabled": true,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"failed_login_count": 2,
"last_failed_login_at": "2026-09-05T10:41:07Z",
"last_login_at": "2026-09-04T22:10:33Z",
"locked_until": "2026-09-05T10:56:07Z",
"status": "dormant",
"disabled_at": null,
"disabled_by": null,
"reactivated_at": "2026-08-01T09:00:00Z",
"dormancy_exempt": false,
"last_activity_at": "2026-07-20T17:12:41Z"
}
]
}| Field | Notes |
|---|---|
failed_login_count |
Consecutive credential failures since the last success or window reset. Always present. |
last_failed_login_at |
RFC 3339 UTC timestamp of the most recent failure. Absent if the account has never failed a login. |
last_login_at |
RFC 3339 UTC timestamp of the most recent successful sign-in. Absent if the account has never signed in. |
locked_until |
RFC 3339 UTC timestamp of lock expiry. Absent when not locked. May be in the past (a stale lock the next attempt will clear). 9999-12-31T00:00:00Z means the lock has no automatic expiry. |
status |
Derived, computed at read time from the account lifecycle fields and the current policy. One of active, locked, disabled, dormant, with precedence disabled > dormant > locked > active. Always present. |
disabled_at |
RFC 3339 UTC timestamp of the most recent admin disable. Absent when enabled. |
disabled_by |
User id of the admin who disabled the account (informational). Absent when enabled. |
reactivated_at |
RFC 3339 UTC timestamp of the most recent admin enable, unlock, or dormancy reactivation. Absent if none has occurred. Feeds the dormancy clock alongside sign-in and API-token activity. |
dormancy_exempt |
true if the account (an administrator account) is marked "never dormant." Always present; meaningful only on admin accounts. |
last_activity_at |
RFC 3339 UTC timestamp of the most recent interactive sign-in or API-token use (see the "Dormant accounts" note in Settings). Absent if the account has never been used. |
The same fields appear on GET /api/auth/me and in the user object returned by login -- a
non-admin only ever sees their own values there.
The user object also includes credential_revision (a non-secret generation value used for
conditional administrative password resets) and protected (whether the target is governed by
the protected-principal workflow). auth_provider identifies local or ldap; clients must
handle absent or unrecognized values as Unknown and must not assume Local.
cURL:
curl https://hub.example.com/api/users \
- H 'Authorization: Bearer <access_token>'Create a new user. A temporary password is auto-generated.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"username": "newuser",
"email": "newuser@example.com",
"role": "viewer"
}Validation:
-
rolemust be"admin","auditor", or"viewer" - Username: 3-32 characters, alphanumeric and underscores only
Response (201):
{
"user": {
"id": "uuid",
"username": "newuser",
"email": "newuser@example.com",
"role": "viewer",
"auth_provider": "local",
"terminal_access": false,
"totp_enabled": false,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"temporary_password": "auto-generated-password"
}Error Cases:
-
400- Invalid username or role -
409- User already exists
cURL:
curl - X POST https://hub.example.com/api/users \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"username":"newuser","email":"newuser@example.com","role":"viewer"}'Update a user's role.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID
Request Body:
{
"role": "auditor"
}Response (200):
{
"user": {
"id": "uuid",
"username": "newuser",
"email": "newuser@example.com",
"role": "admin",
"auth_provider": "local",
"terminal_access": false,
"totp_enabled": false,
"avatar": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}Error Cases:
-
400- Invalid role or attempting to change own role -
404- User not found
Changing an exempt administrator to a non-admin role clears dormancy_exempt in the same request
and writes audit event user.dormancy_exempt_cleared with detail role changed.
cURL:
curl - X PUT https://hub.example.com/api/users/USER_UUID/role \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"role":"admin"}'Disable or enable a user account (feature 008). See Account State Enforcement for where a disabled/dormant account is refused.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID
Request Body:
{
"disabled": true
}Response (200), disabling:
{
"user": {
"id": "uuid",
"status": "disabled",
"disabled_at": "2026-09-06T10:00:00Z",
"disabled_by": "b2c1...",
"...": "…other user fields"
}
}Response (200), enabling ({"disabled": false}): the user object with status: "active" (or
"locked"/"dormant" if still applicable) and reactivated_at set on a real disabled-to-enabled
transition.
Error Cases:
-
400-{"error":"cannot disable your own account"}— target is the caller -
404-{"error":"user not found"} -
409-{"error":"cannot disable the last enabled administrator"}— target is the only remaining account with roleadminand nodisabled_at
Disabling an already-disabled account (or enabling an already-enabled one) returns 200 with the unchanged user — idempotent, but still audited.
Side effects of disabling: token_generation is bumped, so the account's existing web
sessions and refresh tokens are rejected on their next use; all of the account's API tokens
are revoked (not merely suspended — new ones must be minted after re-enable); audit
user.disabled with detail {"revoked_api_tokens":N}. Side effects of enabling:
locked_until and failed_login_count are cleared, reactivated_at is set to now (which also
restarts the dormancy clock), audit user.enabled.
cURL:
curl - X PUT https://hub.example.com/api/users/USER_UUID/status \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"disabled":true}'Clear a locked account's lock and consecutive-failure count (feature 008).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID
Response (200): the user object, status no longer locked and failed_login_count: 0.
Idempotent on an account that is not currently locked (still 200, still audited).
Error Cases:
-
404-{"error":"user not found"}
Audit user.unlocked with detail {"was_locked":true|false}.
cURL:
curl - X POST https://hub.example.com/api/users/USER_UUID/unlock \
- H 'Authorization: Bearer <access_token>'Reset the dormancy clock for a dormant account without enabling a disabled account or clearing a separate lock. Requires an interactive administrator session and cookie CSRF validation; bearer API tokens are not accepted.
There is no request body. Response 200 is { "user": { ... }, "changed": true|false }. An
already active account is an unchanged no-op. A dormant locked account remains Locked. A disabled
account is refused with 409; use Enable for disabled accounts. Protected targets return the
existing structured security approval response (403). The operation is audited.
Generate a temporary password for an ordinary Local account. Requires an interactive administrator
session and cookie CSRF validation; API-token authentication is refused. The target's current
credential_revision must be supplied to avoid resetting a changed account:
{ "expected_revision": 12 }On committed success, response 200 is { "user": { ... }, "temporary_password": "...", "expires_at": "2026-10-28T12:00:00Z" } and the response carries Cache-Control: no-store.
The temporary password is returned once and is not retrievable later. Do not automatically retry
this credential-changing request after an uncertain response. The user must change the password
before normal access; MFA remains enabled and required. Existing sessions, tokens and durable app
authority are revoked. Account lifecycle status is unchanged.
Errors: 400 for self-reset or LDAP/Unknown provider; 403 for a protected target; 409 for a
stale revision; 404 for a missing target. The response never includes the temporary password
unless the authoritative reset commits.
Complete a required password replacement after password and any already-configured TOTP have been
verified. The only accepted bearer is the short-lived password_recovery purpose token returned
from /api/auth/login/totp; ordinary access tokens and API tokens are not accepted.
{ "new_password": "N3wP@ssw0rd!" }Success is { "status": "password updated", "requires_login": true }; cookies are cleared and
the client returns to sign-in. Password policy/history violations return 400; invalid, expired,
or replayed recovery authority returns 401 (a changed account state can return 409). No normal
access or refresh credential is issued by this endpoint.
Active or recent password reuse also records a failed
user.password_reuse_rejected audit event with request correlation and IP;
credential values are never included.
Assign or remove the "never dormant" exemption on an administrator account (feature 008).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID
Request Body:
{
"exempt": true
}Response (200): the user object with the updated dormancy_exempt value.
Error Cases:
-
400-{"error":"dormancy exemption applies to administrator accounts only"}— target's role is notadmin -
404-{"error":"user not found"}
Audit user.dormancy_exempt_set when exempt: true, user.dormancy_exempt_cleared when
exempt: false. The exemption is also cleared automatically (same audit action, detail
role changed) when an exempt account's role is changed away from admin — see endpoint 14 above.
cURL:
curl - X PUT https://hub.example.com/api/users/USER_UUID/dormancy-exemption \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"exempt":true}'Delete a user.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID
Response (200):
{
"status": "user deleted"
}Error Cases:
-
400- Cannot delete your own account -
404- User not found
cURL:
curl - X DELETE https://hub.example.com/api/users/USER_UUID \
- H 'Authorization: Bearer <access_token>'Audit viewing and workflow endpoints are available to admins and auditors. Admins can additionally update audit settings and run retention. The current audit API surface includes:
GET /api/audit-logsGET /api/audit-logs/catalogGET /api/audit-logs/healthGET /api/audit-logs/settings-
PUT /api/audit-logs/settings(admin only) GET /api/audit-logs/exportGET /api/audit-logs/exports-
POST /api/audit-logs/retention/run(admin only; scans at most 1,024 candidates in groups of 256 per call; repeat for large backlogs, and a zero deletion count does not prove a sweep is complete) GET /api/audit-logs/reviewsPOST /api/audit-logs/reviewsGET /api/audit-logs/filtersPOST /api/audit-logs/filtersDELETE /api/audit-logs/filters/{id}GET /api/audit-logs/flagsPOST /api/audit-logs/flagsGET /api/audit-logs/detectionsGET /api/audit-users
The feature-branch governance API uses live capabilities and a protected preview,
step-up, approval, and apply flow. Operations admin is not a security role.
Browser and CLI mutation calls require an interactive session; API tokens cannot
provide fresh action-bound MFA.
| Method and path | Purpose / authority |
|---|---|
GET /api/audit-policy |
Read active policy, readiness, and effective capabilities. Full details require security-read or valid initial-bootstrap authority. |
POST /api/audit-policy/test |
Bounded explicit destination qualification test under the fixed operator-managed endpoint allowlist; it does not activate policy or run a command. |
POST /api/security/secrets |
Write-only destination/password material; returns only an immutable credential_ref and set indicator. There is no read-back route. |
POST /api/security/changes/preview |
Preview {reason, expected_version, change}; returns the canonical proposal and digest. |
POST /api/security/step-up |
Bind fresh MFA to security.request, security.approve, or security.apply, exact digest, and (for approve/apply) exact change ID. |
POST /api/security/changes |
Submit the returned proposal and action-bound step_up_proof. |
GET /api/security/changes?limit=50&offset=0 |
Bounded summaries for live security readers; list page limit is 1–100. |
GET /api/security/changes/{id} |
Read one exact authorized request, including a request where the caller is specifically nominated. Nomination permits this request-scoped review only. |
GET /api/security/principals?limit=50&offset=0&search=... |
Bounded safe principal selector for security readers or initial-bootstrap authority; search is limited and does not expose credentials or LDAP details. |
POST /api/security/changes/{id}/approve |
Record one independent-person approval with a proof bound to this request and digest. |
POST /api/security/changes/{id}/apply |
Apply an approved exact change using a current-version compare-and-swap and fresh proof. |
POST /api/security/changes/{id}/reconcile |
Resume reconciliation of that existing application decision; it does not create a new decision or bypass quorum. |
Changes are typed (bootstrap, policy, role, person, recovery,
audit-settings, or ldap). Reasons and expected positive policy versions are
required. The baseline quorum is the requester and two approvers linked to
distinct verified person identities. Editing a request invalidates prior votes;
stale, expired, revoked, or digest-mismatched approvals do not count. The server
rechecks live authority at approval and application.
The recovery operation password-reset accepts local accounts only. LDAP
targets are refused during preview and request, and checked again inside apply
and password completion transactions. Directory passwords must be recovered in
the directory.
Bootstrap nominations include the requester as officer and two other people as
approvers, all with verified person IDs, operator evidence, and completed MFA.
An initial nominee may still be an ordinary viewer without global
security.read or security.approve. Such a nominee can review only the exact
nominated request ID through the request-specific route; nomination never grants
broad request listing, policy visibility, principal lookup, or global
capabilities. Do not use global /auth/me capabilities to infer the rights for a
particular nominated request.
Credentials are uploaded separately and never appear in request diffs or policy readback. Credential-bearing URLs, private keys, and secret values do not belong in proposal files or reasons. A fresh proof authorizes only its action, request, and digest, and is not a reusable bearer token. See Settings and CLI for the operator workflows.
GET /api/executions and GET /api/executions/{id} require the live audit.read
capability. They return redacted execution metadata: actor, target, hashes and
lengths, policy version, process/evidence state, and prepared OS identity. An
attempt refused before preparation has preparation: null. These responses
exclude command/script bytes, output, receipt tokens, and archive references.
List filters are actor_id, node_id, os_user, policy_version, state, and
evidence_state; limit defaults to 50 and is capped at 100, with offset
pagination.
Standard SSH command preparation or admission failures return a diagnostic on stderr and a nonzero exit status to a responsive client; Enhanced mode provides this response after complete input collection. Cancellation during Enhanced collection may instead close the channel. Inspect the redacted execution record for its durable state. An admission failure does not establish that a process ran. After an attempted launch, a transport failure can leave the remote outcome uncertain. Once finalization begins for an accepted execution, final SSH response delivery and relay cleanup have a deadline of at most two seconds, with SSH connection closure if response delivery stalls. Do not automatically retry an uncertain execution.
Original access requires a live interactive session with audit.original.read,
enrolled MFA, and a fresh action-bound TOTP proof. Each request selects exactly
one envelope or script, with a nonempty purpose of at most 1,024 UTF-8 bytes.
| Method and path | Request / response |
|---|---|
POST /api/executions/{id}/original-access/preview |
{purpose, records: [kind]} → {action, digest, records: [{kind, event_id, sha256, bytes, version_id}]}
|
POST /api/executions/{id}/original-access/step-up |
Same selection plus digest and code → {proof, expires_at}; proof expires after five minutes |
POST /api/executions/{id}/original-access |
Same selection plus digest and step_up_proof → 201 {handle, expires_at, download_url}
|
GET /api/audit-originals/{handle} |
Downloads the selected exact archived version once; Cache-Control: no-store, private
|
The handle expires within 60 seconds, is bound to the actor and session, and is
consumed durably before the archive opens. Permission, identity revision, and
session validity are checked again before bytes are delivered. Do not retry a
download automatically, including after a 401, disconnect, or archive error;
obtain a new preview, fresh MFA proof, and handle instead. API tokens cannot use
the preview, step-up, authorization, or download routes.
Required intent and start evidence must be preserved externally before original
bytes are read. Results distinguish completed, interrupted, and unconfirmed
delivery. delivery.scope is hub_response; bytes_written: null means the byte
count is unknown. A completed result measures bytes accepted by the Hub response
writer, without asserting that a client saved them. Archive/evidence failures
return opaque 503 errors; missing permission, expired authority, or replay
returns 403 (an expired login may return 401).
Local original-access metadata is bounded to 4,096 records, with confirmed or permanently incomplete history pruned after 30 days. Pending evidence is bounded to 1,024 records / 16 MiB, retried with a one-minute backoff for at most 24 hours. Capacity exhaustion refuses access; expired recovery becomes permanently incomplete and never recreates a download handle. The original objects retain their separately approved external retention.
List audit log entries with optional filters.
| Property | Value |
|---|---|
| Auth | Access token (cookie or Bearer), admin or auditor |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
int | Max entries to return (default 50, max 100) |
offset |
int | Number of entries to skip (default 0) |
action |
string | Filter by action type (e.g., user.login, server.created) |
user_id |
string | Filter by user UUID |
from |
string | Start datetime (ISO 8601) |
to |
string | End datetime (ISO 8601) |
Available action types:
User actions: user.login, user.login_failed, user.login_totp_failed, user.registered, user.totp_setup, user.totp_enabled, user.totp_disabled, user.created, user.totp_reset, user.password_changed, user.password_reuse_rejected, user.role_updated, user.deleted
Server actions: server.created, server.updated, server.deleted, server.batch_deleted, server.registered, server.registration_failed, server.connected, server.disconnected, server.unregistered
File/path actions: file.read, file.uploaded, path.granted, path.revoked
Log actions: log.tail_started
Terminal actions: terminal.opened, terminal.closed
API token actions: api_token.created, api_token.revoked
Audit governance actions: audit.exported, audit.review_completed, audit.filter_saved, audit.filter_deleted, audit.retention_updated, audit.retention_executed, audit.flag_created
Response (200):
{
"entries": [
{
"id": "uuid",
"user_id": "uuid",
"action": "user.login",
"target": null,
"detail": null,
"ip_address": "192.168.1.1",
"created_at": "2025-01-01T00:00:00Z"
}
],
"total": 150,
"limit": 50,
"offset": 0
}cURL:
curl 'https://hub.example.com/api/audit-logs?limit=20&action=user.login' \
- H 'Authorization: Bearer <access_token>'List servers. Admins see all servers; viewers see only servers they have path permissions on.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
int | Max servers to return (default 50, max 100) |
offset |
int | Number of servers to skip (default 0) |
status |
string | Filter by status (pending, online, offline) |
search |
string | Search by server name |
Response (200):
{
"servers": [
{
"id": "uuid",
"name": "web-prod-1",
"hostname": "web-prod-1.example.com",
"ip_address": "10.0.1.5",
"os": "linux",
"status": "online",
"agent_version": "1.0.0",
"labels": "{\"env\":\"production\"}",
"last_seen_at": "2025-01-01T12:00:00Z",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
],
"total": 10,
"limit": 50,
"offset": 0
}cURL:
curl 'https://hub.example.com/api/servers?status=online&search=prod' \
- H 'Authorization: Bearer <access_token>'Create a new server entry and get back a registration token and install command.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"name": "web-prod-1",
"labels": "{\"env\":\"production\"}"
}The labels field is optional and defaults to "{}".
Response (201):
{
"server": {
"id": "uuid",
"name": "web-prod-1",
"hostname": null,
"ip_address": null,
"os": null,
"status": "pending",
"agent_version": null,
"labels": "{\"env\":\"production\"}",
"last_seen_at": null,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
},
"registration_token": "raw-uuid-token",
"install_command": "curl - sSL https://hub.example.com/install.sh | sudo bash - s -- --token <token> --hub hub.example.com:443 --url https://hub.example.com"
}Note: registration_token is the raw token. The Hub stores only a SHA-256 hash. The token expires after 1 hour.
Error Cases:
-
400- Server name is required
cURL:
curl - X POST https://hub.example.com/api/servers \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"name":"web-prod-1","labels":"{\"env\":\"production\"}"}'Get a single server by ID. Viewers must have path permissions on the server.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Path Parameters:
-
id- Server UUID
Response (200):
{
"id": "uuid",
"name": "web-prod-1",
"hostname": "web-prod-1.example.com",
"ip_address": "10.0.1.5",
"os": "linux",
"status": "online",
"agent_version": "1.0.0",
"labels": "{\"env\":\"production\"}",
"last_seen_at": "2025-01-01T12:00:00Z",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}Error Cases:
-
403- Access denied (viewer without permissions) -
404- Server not found
cURL:
curl https://hub.example.com/api/servers/SERVER_UUID \
- H 'Authorization: Bearer <access_token>'Update a server's name and labels.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Request Body:
{
"name": "web-prod-1-updated",
"labels": "{\"env\":\"staging\"}"
}Response (200): Returns the updated server object (same shape as GET /api/servers/{id}).
Error Cases:
-
400- Server name is required -
404- Server not found
cURL:
curl - X PUT https://hub.example.com/api/servers/SERVER_UUID \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"name":"web-prod-1-updated","labels":"{\"env\":\"staging\"}"}'Delete a server from the database (does not uninstall the agent).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Response (200):
{
"status": "deleted"
}Error Cases:
-
404- Server not found
cURL:
curl - X DELETE https://hub.example.com/api/servers/SERVER_UUID \
- H 'Authorization: Bearer <access_token>'Delete multiple servers at once.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"ids": ["uuid-1", "uuid-2", "uuid-3"]
}Response (200):
{
"status": "deleted",
"deleted": 3
}Error Cases:
-
400- IDs list cannot be empty -
500- Failed to delete servers
cURL:
curl - X POST https://hub.example.com/api/servers/batch-delete \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"ids":["uuid-1","uuid-2"]}'Path permissions control which filesystem paths a viewer-role user can access on a given server. Admin users have unrestricted access to all paths.
List all path permissions for a server.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Response (200):
{
"paths": [
{
"id": "perm-uuid",
"user_id": "user-uuid",
"username": "viewer1",
"server_id": "server-uuid",
"path": "/var/log/nginx",
"created_at": "2025-01-01T00:00:00Z"
}
]
}cURL:
curl https://hub.example.com/api/servers/SERVER_UUID/paths \
- H 'Authorization: Bearer <access_token>'Grant a user access to a filesystem path on a server.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Request Body:
{
"user_id": "user-uuid",
"path": "/var/log/nginx"
}Response (201):
{
"id": "perm-uuid",
"user_id": "user-uuid",
"server_id": "server-uuid",
"path": "/var/log/nginx",
"created_at": "2025-01-01T00:00:00Z"
}Error Cases:
-
400-user_idandpathare required -
409- Permission already exists or invalid reference
cURL:
curl - X POST https://hub.example.com/api/servers/SERVER_UUID/paths \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"user_id":"USER_UUID","path":"/var/log/nginx"}'Revoke a path permission.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID -
pathId- Permission UUID
Response: 204 No Content
Error Cases:
-
404- Permission not found (or does not belong to this server)
cURL:
curl - X DELETE https://hub.example.com/api/servers/SERVER_UUID/paths/PERM_UUID \
- H 'Authorization: Bearer <access_token>'Get the current user's allowed paths for a server. Admins always receive ["/"] (full access).
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Path Parameters:
-
id- Server UUID
Response (200):
{
"paths": ["/var/log/nginx", "/var/log/syslog"]
}For admins:
{
"paths": ["/"]
}cURL:
curl https://hub.example.com/api/servers/SERVER_UUID/my-paths \
- H 'Authorization: Bearer <access_token>'These endpoints proxy requests to a connected agent via gRPC. The agent must be online and connected.
Browse files on a remote server.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), permission-checked |
Path Parameters:
-
id- Server UUID
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute directory path to list (default: /) |
Path validation:
- Must be an absolute path (starts with
/) - Path traversal (
..) is not allowed - Viewers must have a permission covering the requested path
Response (200):
{
"files": [
{
"name": "access.log",
"path": "/var/log/nginx/access.log",
"is_dir": false,
"size": 1048576,
"mod_time": "2025-01-01T12:00:00Z",
"mode": "0644"
}
]
}Error Cases:
-
400- Invalid path (not absolute, contains..) -
403- Access denied (viewer without matching permission) -
404- Path not found on agent -
502- Agent communication failure
cURL:
curl 'https://hub.example.com/api/servers/SERVER_UUID/files?path=/var/log' \
- H 'Authorization: Bearer <access_token>'Read the contents of a file from a remote server. Returns base64-encoded data.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), permission-checked |
Path Parameters:
-
id- Server UUID
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute file path to read (required) |
Limits:
- Reads up to 1MB of file content
- Files larger than 10MB cannot be viewed (returns 413)
Response (200):
{
"data": "base64-encoded-file-content",
"total_size": 1048576,
"mime_type": "text/plain"
}Error Cases:
-
400- Path is required, path must be absolute, path traversal not allowed -
403- Access denied -
404- File not found on agent -
413- File too large for viewing (> 10MB) -
502- Agent communication failure
cURL:
curl 'https://hub.example.com/api/servers/SERVER_UUID/files/read?path=/var/log/syslog' \
- H 'Authorization: Bearer <access_token>'Tail a log file in real-time via Server-Sent Events (SSE). Each event contains base64-encoded log data.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), permission-checked |
Path Parameters:
-
id- Server UUID
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
path |
string | Absolute file path to tail (required) |
grep |
string | Optional filter string - only lines matching this are streamed |
Response: 200 with Content-Type: text/event-stream
Each SSE message has the format:
data: <base64-encoded-chunk>\n\n
The connection stays open until the client disconnects. The Hub sends a stop command to the agent on disconnect.
Error Cases:
-
400- Path is required -
403- Access denied -
502- Failed to send request to agent
cURL:
curl - N 'https://hub.example.com/api/servers/SERVER_UUID/logs/tail?path=/var/log/syslog&grep=error' \
- H 'Authorization: Bearer <access_token>'Upload a file to a server's dropzone (/tmp/veyport-dropzone/ on the agent).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
| Content-Type | multipart/form-data |
Path Parameters:
-
id- Server UUID
Form Fields:
-
file- The file to upload (max 100MB)
Response (200):
{
"filename": "config.tar.gz",
"size": 2048576
}Error Cases:
-
400- No file provided, filename is required -
413- File too large (max 100MB) -
502- Agent communication failure -
504- Upload timeout (30s)
cURL:
curl - X POST https://hub.example.com/api/servers/SERVER_UUID/upload \
- H 'Authorization: Bearer <access_token>' \
- F 'file=@/path/to/local/config.tar.gz'List files in a server's dropzone directory (/tmp/veyport-dropzone/).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Response (200):
{
"files": [
{
"name": "config.tar.gz",
"path": "/tmp/veyport-dropzone/config.tar.gz",
"is_dir": false,
"size": 2048576,
"mod_time": "2025-01-01T12:00:00Z",
"mode": "0644"
}
]
}Returns an empty list if the dropzone directory does not exist yet.
cURL:
curl https://hub.example.com/api/servers/SERVER_UUID/dropzone \
- H 'Authorization: Bearer <access_token>'Delete a file from a server's dropzone.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
filename |
string | Name of the file to delete (required) |
Response: 204 No Content
Error Cases:
-
400- Filename is required -
500- Delete failed on agent
cURL:
curl - X DELETE 'https://hub.example.com/api/servers/SERVER_UUID/dropzone?filename=config.tar.gz' \
- H 'Authorization: Bearer <access_token>'These endpoints are public (no authentication required) and are used by the install script.
Download the agent installation shell script.
| Property | Value |
|---|---|
| Auth | None |
| Content-Type (response) |
text/x-sh (served as static file) |
cURL:
curl - sSL https://hub.example.com/install.shDownload the agent binary for a specific platform.
| Property | Value |
|---|---|
| Auth | None |
| Content-Type (response) | application/octet-stream |
Path Parameters:
-
os- Operating system (onlylinuxis supported) -
arch- Architecture (amd64orarm64)
Response: Binary file download with Content-Disposition: attachment; filename=veyport-agent-linux-amd64
Error Cases:
-
404- Unsupported platform or binary not found
cURL:
curl - O https://hub.example.com/install/linux/amd64Download the vey CLI install script. This is the script behind the one-line install shown in the web UI under Install CLI on the dashboard; see CLI for the walkthrough. The Hub renders its own public address into the script, so the one-liner takes no arguments and no token — the CLI authenticates later via vey login.
| Property | Value |
|---|---|
| Auth | None |
| Content-Type (response) | text/x-shellscript; charset=utf-8 |
Response: A POSIX sh script that detects the platform (Linux/macOS, amd64/arm64), downloads the matching binary from /install/cli/{os}/{arch}, verifies it against /install/cli/{os}/{arch}/sha256 before installing, and installs to /usr/local/bin when writable (offering sudo only on an interactive terminal), otherwise to ~/.local/bin with a PATH hint. Re-running it upgrades an existing install in place.
The Hub address baked into the script comes from VEYPORT_PUBLIC_BASE_URL if set, then the stored public_base_url setting, then the request's own host.
Error Cases:
-
500- The Hub's public address is not configured or is not a validhttp/httpsURL
cURL:
curl -fsSL https://hub.example.com/install/cli.sh | shDownload the vey CLI binary for a specific platform. See CLI for the full install walkthrough and command reference.
| Property | Value |
|---|---|
| Auth | None |
| Content-Type (response) | application/octet-stream |
Path Parameters:
-
os- Operating system (linuxordarwin) -
arch- Architecture (amd64orarm64)
Response: Binary file download with Content-Disposition: attachment; filename="vey-{os}-{arch}"
Error Cases:
-
404- Unsupported platform (outside the{linux,darwin}x{amd64,arm64}allowlist) or binary not built for this Hub
cURL:
curl -o vey https://hub.example.com/install/cli/linux/amd64Get the SHA-256 checksum of the vey CLI binary for a specific platform, for verifying the download from the endpoint above.
| Property | Value |
|---|---|
| Auth | None |
| Content-Type (response) | text/plain |
Path Parameters:
-
os- Operating system (linuxordarwin) -
arch- Architecture (amd64orarm64)
Response: The hex-encoded SHA-256 checksum as plain text.
Error Cases:
-
404- Unsupported platform or checksum not found
cURL:
curl https://hub.example.com/install/cli/linux/amd64/sha256Unregister a server: sends an unregister command to the agent (if connected), then deletes the server from the database.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
The Hub attempts to notify the connected agent to clean up. If the agent does not respond within 10 seconds, the server is still deleted from the database.
Response (200):
{
"status": "unregistered"
}Error Cases:
-
500- Failed to delete server from database
cURL:
curl - X DELETE https://hub.example.com/api/servers/SERVER_UUID/unregister \
- H 'Authorization: Bearer <access_token>'Public endpoint called by the agent during reinstallation. The server UUID in the path acts as proof of installation. Disconnects the agent (if connected) and deletes the server from the database.
| Property | Value |
|---|---|
| Auth | None |
Path Parameters:
-
id- Server UUID
Response: 204 No Content
If the server has already been deleted, also returns 204 No Content.
Error Cases:
-
500- Failed to delete server from database
cURL:
curl - X DELETE https://hub.example.com/api/servers/SERVER_UUID/self-unregisterGet the Hub configuration (currently the gRPC external address and the account lockout/dormancy policy).
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"grpc_external_addr": "veyport.example.com:9443",
"jwt_secret_rotated_at": "2026-06-12T04:10:00Z",
"lockout_threshold": 5,
"lockout_window_minutes": 15,
"lockout_duration_minutes": 15,
"dormant_days": 35,
"session_idle_minutes": 15,
"cli_session_idle_minutes": 45,
"session_max_hours": 12
}| Field | Notes |
|---|---|
grpc_external_addr |
Configured external gRPC address |
jwt_secret_rotated_at |
RFC 3339 UTC timestamp of the last admin rotate-jwt-secret run. null until the secret has been rotated at least once. Read-only — ignored on PUT. |
lockout_threshold |
Effective consecutive-failure count that locks an account. Built-in default 5 when unset. |
lockout_window_minutes |
Effective window, in minutes, that failures count toward the threshold before it restarts at 1. Built-in default 15. |
lockout_duration_minutes |
Effective lock length in minutes. Built-in default 15. 0 means a lock does not auto-expire. |
dormant_days |
Effective number of days of inactivity (no interactive sign-in and no API-token use) after which an account is treated as dormant. Built-in default 35. 0 disables dormancy entirely — no account is ever evaluated as dormant. |
session_idle_minutes |
Effective idle timeout, in minutes, for web sessions (feature 009). Built-in default 15. 0 disables the web idle limit. |
cli_session_idle_minutes |
Effective idle timeout, in minutes, for CLI sessions. Built-in default 45. 0 disables the CLI idle limit. The recorded session kind selects which idle policy applies. |
session_max_hours |
Effective absolute lifetime, in hours, for server-side sessions (feature 009). Built-in default 12. 0 disables the absolute limit. Applies to sessions created after the change; an existing session keeps the lifetime it was issued with. |
cURL:
curl https://hub.example.com/api/settings/hub \
- H 'Authorization: Bearer <access_token>'Update the Hub configuration.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"grpc_external_addr": "veyport.example.com:9443",
"lockout_threshold": 3,
"lockout_window_minutes": 15,
"lockout_duration_minutes": 1,
"dormant_days": 30,
"session_idle_minutes": 1,
"cli_session_idle_minutes": 45,
"session_max_hours": 1
}Validation:
- All fields are optional. A field left out of the request body is unchanged.
-
grpc_external_addr, if present and non-empty, must match^[a-zA-Z0-9._:\-\[\]]+$(hostnames, IPs, and ports only -- no shell metacharacters). A present-but-empty string ("") clears the stored address; omitting the field entirely leaves it unchanged. -
lockout_threshold,lockout_window_minutes,lockout_duration_minutes,dormant_daysare each optional non-negative integers.lockout_threshold: 0disables locking (failures are still counted);lockout_duration_minutes: 0means a lock never auto-expires;dormant_days: 0disables dormancy. Changes apply to future evaluations only -- an account already locked keeps its original expiry, and an account already dormant stays dormant until an admin enables it, even if the policy is raised afterward. -
session_idle_minutes,cli_session_idle_minutes, andsession_max_hoursare optional non-negative integers. The first controls web sessions (default15), the second controls CLI sessions (default45), andsession_max_hoursis the shared absolute lifetime (default12). Either idle field set to0disables that kind's idle limit. The recorded session kind selects the idle policy. An idle change applies on the next evaluation of sessions of that kind; an absolute-lifetime change applies only to sessions created afterward -- an existing session keeps theexpires_atit was issued with. -
cli_session_idle_minutesmust be at most153722867minutes so its duration remains representable. Larger values return400before any settings are written.
Response (200):
{
"status": "ok"
}Error Cases:
-
400- Invalid gRPC address format -
400- Invalid lockout field, field-specific message, e.g.{"error":"lockout_threshold must be a non-negative integer"} -
400-{"error":"dormant_days must be a non-negative integer"} -
400-{"error":"session_idle_minutes must be a non-negative integer"},{"error":"cli_session_idle_minutes must be a non-negative integer"}, or{"error":"session_max_hours must be a non-negative integer"} -
400-{"error":"cli_session_idle_minutes exceeds the maximum supported duration"}
cURL:
curl - X PUT https://hub.example.com/api/settings/hub \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"grpc_external_addr":"veyport.example.com:9443"}'Get the current LDAP configuration. The bind password is write-only and is never returned; use bind_password_set to tell whether a password is already stored.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"enabled": true,
"url": "ldaps://freeipa.example.com:636",
"bind_dn": "uid=veyport,cn=sysaccounts,cn=etc,dc=example,dc=com",
"bind_password": "",
"bind_password_set": true,
"user_base_dn": "cn=users,cn=accounts,dc=example,dc=com",
"group_base_dn": "cn=groups,cn=accounts,dc=example,dc=com",
"user_search_filter": "(uid={username})",
"group_search_filter": "(|(member={dn})(memberUid={username}))",
"username_attribute": "uid",
"email_attribute": "mail",
"external_id_attribute": "entryUUID",
"group_name_attribute": "cn",
"start_tls": false,
"tls_server_name": "freeipa.example.com",
"ca_cert_pem": "",
"allow_insecure_transport": false,
"admin_groups": ["freeipa-admins"],
"auditor_groups": ["freeipa-auditors"],
"viewer_groups": ["freeipa-viewers"],
"terminal_groups": ["bastion-users"]
}Update LDAP login, search, TLS, and group mapping settings. The bind password is encrypted before storage. Omit bind_password or send an empty string to keep the existing password; send clear_bind_password: true to clear it.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"enabled": true,
"url": "ldaps://freeipa.example.com:636",
"bind_dn": "uid=veyport,cn=sysaccounts,cn=etc,dc=example,dc=com",
"bind_password": "service-account-password",
"user_base_dn": "cn=users,cn=accounts,dc=example,dc=com",
"group_base_dn": "cn=groups,cn=accounts,dc=example,dc=com",
"user_search_filter": "(uid={username})",
"group_search_filter": "(|(member={dn})(memberUid={username}))",
"username_attribute": "uid",
"email_attribute": "mail",
"external_id_attribute": "entryUUID",
"group_name_attribute": "cn",
"start_tls": false,
"tls_server_name": "freeipa.example.com",
"ca_cert_pem": "",
"allow_insecure_transport": false,
"admin_groups": ["freeipa-admins"],
"auditor_groups": ["freeipa-auditors"],
"viewer_groups": ["freeipa-viewers"],
"terminal_groups": ["bastion-users"]
}Validation:
- Enabled LDAP requires URL, user base DN, group base DN, and at least one role group
- Plain
ldap://requires StartTLS unlessallow_insecure_transportis true - Bind DN requires a stored or submitted bind password
- CA certificate PEM must parse as a valid certificate bundle when provided
Response (200):
{
"status": "ok"
}Test the submitted LDAP URL, TLS settings, and service bind credentials without requiring an end-user password.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body: Same schema as PUT /api/settings/ldap.
Response (200):
{
"status": "ok"
}Error Cases:
-
400- Invalid LDAP configuration -
502- LDAP connection or service bind failed
Get the current SMTP configuration. The password field is omitted from the response for security.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"host": "smtp.example.com",
"port": 587,
"username": "notifications@example.com",
"from": "Veyport <notifications@example.com>",
"tls": true,
"enabled": true
}cURL:
curl https://hub.example.com/api/settings/smtp \
- H 'Authorization: Bearer <access_token>'Update the SMTP configuration. Invalidates the cached SMTP config immediately.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"host": "smtp.example.com",
"port": 587,
"username": "notifications@example.com",
"password": "smtp-password",
"from": "Veyport <notifications@example.com>",
"tls": true,
"enabled": true
}Response (200):
{
"status": "ok"
}cURL:
curl - X PUT https://hub.example.com/api/settings/smtp \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"host":"smtp.example.com","port":587,"username":"user","password":"pass","from":"Veyport <user@example.com>","tls":true,"enabled":true}'Send a test email to verify SMTP configuration. Requires SMTP settings to be saved first.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Request Body:
{
"recipient": "admin@example.com"
}Response (200):
{
"status": "ok"
}Error Cases:
-
400- Invalid request body, missing recipient, or SMTP not configured -
500- SMTP send failure (error details logged server-side)
cURL:
curl - X POST https://hub.example.com/api/settings/smtp/test \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"recipient":"admin@example.com"}'Get the current user's notification preferences. Returns all 8 event types with their enabled/disabled state.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Response (200):
{
"preferences": [
{
"event_type": "agent.offline",
"label": "Agent went offline",
"category": "Agent",
"enabled": true
},
{
"event_type": "security.login_failed",
"label": "Failed login attempt",
"category": "Security",
"enabled": true
}
]
}cURL:
curl https://hub.example.com/api/notifications/preferences \
- H 'Authorization: Bearer <access_token>'Update the current user's notification preferences.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) |
Request Body:
{
"preferences": [
{ "event_type": "agent.offline", "enabled": true },
{ "event_type": "security.login_failed", "enabled": false }
]
}Validation:
- Each
event_typemust be one of the 8 known event types. Unknown types return400.
Response (200):
{
"status": "ok"
}Error Cases:
-
400- Invalid request body or unknown event type
cURL:
curl - X PUT https://hub.example.com/api/notifications/preferences \
- H 'Authorization: Bearer <access_token>' \
- H 'Content-Type: application/json' \
- d '{"preferences":[{"event_type":"agent.offline","enabled":true}]}'List notification delivery log entries. Supports pagination.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
int | Max entries to return (default 50) |
offset |
int | Number of entries to skip (default 0) |
Response (200):
{
"entries": [
{
"id": "uuid",
"user_id": "user-uuid",
"username": "admin",
"event_type": "agent.offline",
"subject": "Agent web-prod-1 went offline",
"status": "sent",
"error": null,
"created_at": "2025-06-15T10:30:00Z"
}
],
"total": 42,
"limit": 50,
"offset": 0
}cURL:
curl 'https://hub.example.com/api/notifications/log?limit=20' \
- H 'Authorization: Bearer <access_token>'Re-enrollment endpoints are admin-only and require an access token. They manage the workflow for nodes whose mTLS client certificates have expired while offline.
List all servers with a pending re-enrollment request.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Response (200):
{
"requests": [
{
"request_id": "uuid",
"server_id": "uuid",
"server_name": "web-prod-1",
"hostname": "web-prod-1.example.com",
"ip_address": "10.0.1.5",
"requested_at": "2026-07-01T03:00:00Z",
"clone_suspected": false
}
]
}cURL:
curl https://hub.example.com/api/servers/reenroll/pending \
-H 'Authorization: Bearer <access_token>'Approve one exact pending re-enrollment request. Requires the approving admin's current TOTP code (step-up authentication). Approval releases a challenge exchange on that request's originating bootstrap stream; the Hub issues a new certificate only after verifying the node's proof on that stream. The bootstrap stream cannot enter normal routing. The node reconnects using the issued certificate, which the Hub verifies before accepting normal agent traffic.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Request Body:
{
"request_id": "<uuid>",
"totp_code": "123456"
}Response (200):
{
"status": "approved"
}Error Cases:
| Status | Meaning |
|---|---|
401 |
Missing or invalid access token |
403 |
Bad or missing TOTP code (step-up failed) |
404 |
Server or request_id not found |
409 |
re-register required — node has no transport key; it was enrolled before transport-key support and must be re-registered once |
cURL:
curl -X POST https://hub.example.com/api/servers/SERVER_UUID/reenroll/approve \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"request_id":"REQUEST_UUID","totp_code":"123456"}'Deny a pending re-enrollment request. The node will not receive a new certificate.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Server UUID
Request Body:
{
"request_id": "<uuid>"
}Response (200):
{
"status": "denied"
}Error Cases:
-
404- Server or request_id not found
cURL:
curl -X POST https://hub.example.com/api/servers/SERVER_UUID/reenroll/deny \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"request_id":"REQUEST_UUID"}'Terminal endpoints require an interactive browser access token. CLI-created API tokens are rejected. Access is limited to admins and LDAP users who have terminal access from LDAP group mapping plus a root (/) assignment on the target server.
Create a terminal session on an online server.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
| Rate Limit | 3600 requests/minute shared across terminal endpoints |
Path Parameters:
-
id- Server UUID
Request Body:
{
"cols": 120,
"rows": 32,
"cwd": "/home/alice"
}cwd is optional. If provided, it must be a valid absolute path.
Response (201):
{
"session_id": "session-uuid"
}Error Cases:
-
400- Invalid request body or invalidcwd -
403- Terminal access required, interactive login required, or root assignment required -
502- Agent failed to open the terminal -
504- Agent did not respond in time
Attach to a terminal session event stream. Only one stream can attach to a session.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
| Response Type | text/event-stream |
SSE Data Events:
data: <base64-encoded-terminal-bytes>
SSE Exit Events:
event: exit
data: {"exit_code":0,"error":""}
The Hub closes the terminal session when the stream disconnects.
Error Cases:
-
404- Terminal session not found -
409- Stream already attached
Send terminal input to an active session.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
Request Body:
{
"data": "whoami\n"
}data is limited to 8192 bytes per request.
Response: 202 Accepted
Error Cases:
-
400- Invalid request body or input too large -
404- Terminal session not found -
409- Terminal session is closed -
502- Failed to send input to the agent
Resize an active terminal session.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
Request Body:
{
"cols": 120,
"rows": 32
}Response: 202 Accepted
Error Cases:
-
400- Invalid request body -
404- Terminal session not found -
409- Terminal session is closed -
502- Failed to send resize event to the agent
Close an active terminal session.
| Property | Value |
|---|---|
| Auth | Interactive access token, terminal access required |
Response: 204 No Content
Error Cases:
-
404- Terminal session not found
These two endpoints support the native SSH gateway (see SSH Gateway): issuing a short-lived user certificate and publishing the gateway's host identity for pinning. The gateway itself is a raw SSH listener on a separate port (default :2222), not an HTTP endpoint — these are the only two HTTP touchpoints for it.
Issue a short-lived SSH user certificate for the caller's own identity, for use against the SSH gateway.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), interactive session only — API tokens are refused (SC-004) |
| Rate Limited | Yes (10 requests / 60s per caller) |
Request Body:
{
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... client-key"
}public_key must be an authorized_keys-form ed25519, ECDSA, or RSA key (not a certificate). The principal on the issued certificate is always the caller's own username, resolved from the access token — it cannot be requested in the body.
Response (200):
{
"certificate": "ssh-ed25519-cert-v01@openssh.com AAAA...",
"principal": "alice",
"expires_at": "2026-08-06T20:14:03Z",
"host_key_fingerprint": "SHA256:AbCdEf...",
"gateway_port": 2222
}Error Cases:
-
400- Invalid or unsupported public key, or an existing certificate submitted as the subject key -
401- No or invalid access token -
403-{"error":"interactive login required"}— the credential is an API token, not an interactive session (auditedssh.cert_issue_refused) -
403- Account disabled or dormant, same messages as login (auditedssh.cert_issue_refused); see Account State Enforcement -
429- Rate limit exceeded -
503- SSH gateway disabled by configuration, or its key material is unavailable
Successful issuance is audited ssh.cert_issued with principal and TTL.
cURL:
curl -X POST https://hub.example.com/api/ssh/certificates \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{"public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... client-key"}'Get the SSH gateway's host identity, for pinning instead of trust-on-first-use.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) — any authenticated caller, including API tokens; this is public trust material, not a capability |
| Rate Limited | No |
Response (200):
{
"fingerprint": "SHA256:AbCdEf...",
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... host-key",
"port": 2222
}Stable across Hub restarts and upgrades — the host key is never silently regenerated (FR-006).
Error Cases:
-
503- SSH gateway key material is unavailable
cURL:
curl https://hub.example.com/api/ssh/host-key \
-H 'Authorization: Bearer <access_token>'Feature 009 gives every completed interactive sign-in (web browser or vey CLI) a server-side
session record, so it can be listed, timed out, and ended precisely -- not just invalidated
all-at-once via the token-generation bump described under Session Invalidation on Password
Change in the engineering security model. API tokens (adt_…) are not sessions: they keep their
own expiry and revocation (see endpoint 15's neighbours and CLI) and are unaffected by
everything on this page.
Every request bearing an access token, and every POST /api/auth/refresh, re-validates the
caller's session before anything else runs:
| Condition | Response |
|---|---|
No sid claim, or the session it names no longer exists (pre-upgrade credentials) |
401 {"error":"session expired — sign in again"} |
| Session ended (revoked by an admin, by the user, by logout, or by an account disable) | 401 {"error":"session ended — sign in again"} |
| Absolute lifetime reached |
401 {"error":"session expired — sign in again"} (session marked expired_absolute, audited once on first detection) |
| Idle limit exceeded |
401 {"error":"session expired — sign in again"} (session marked expired_idle, audited once on first detection); the recorded kind selects the web or CLI idle limit |
| None of the above | request proceeds; last_seen_at writes are throttled to min(60 seconds, selected idle limit / 10), or 60 seconds when idle expiry is disabled |
Upgrade note: a session that existed before this release has no session record, so its access
and refresh tokens are refused with the session expired message the first time they're used
after upgrading -- every user, web and CLI, signs in once more. There is no migration path for
pre-upgrade sessions; see Troubleshooting ("everyone had to sign in again after upgrading").
The web and CLI idle limits are configured as session_idle_minutes and
cli_session_idle_minutes, respectively; the shared absolute limit is session_max_hours on the
Account policy card alongside the 007/008 lockout and dormancy fields -- see endpoints 37/38
above. Idle expiry does not cancel an SSH command already running; it requires a new sign-in for
later CLI requests. Signed-in browser pages use HEAD /api/auth/me as a throttled activity check
following recent interaction in a visible tab. HEAD returns the same status as GET
without a response body, avoiding repeated profile and avatar downloads. It uses the same session checks
as other authenticated requests: it cannot revive an expired or revoked session,
and it cannot extend the absolute lifetime.
List the caller's own sessions.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), interactive session only (an API token is refused) |
Response (200):
{
"sessions": [
{"id":"…","kind":"web","ip":"10.0.0.5","user_agent":"Mozilla/…","created_at":"…","last_seen_at":"…","expires_at":"…","idle_deadline_at":"…","current":true},
{"id":"…","kind":"cli","ip":"…","user_agent":"vey/2.0.37","created_at":"…","last_seen_at":"…","expires_at":"…","idle_deadline_at":"…","current":false},
{"id":"shell:srv-01:sess-9f2","kind":"ssh","server":"web-01","started_at":"…","last_activity_at":"…","current":false}
]
}Only the caller's own rows; the row for the session making this call is marked current: true.
There is no include_ended on the self-service view -- ended sessions are an admin-only history.
Shell rows (kind: "ssh" or "terminal") use the id shell:<serverID>:<sessionID> and carry
server, started_at, last_activity_at in place of the web/CLI fields.
cURL:
curl https://hub.example.com/api/auth/sessions \
-H 'Authorization: Bearer <access_token>'End one of the caller's other sessions or shells.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), interactive session only |
Path Parameters:
-
sid- Session id, orshell:<serverID>:<sessionID>for an open SSH or web-terminal shell
Response (200):
{"status": "ended"}{"status": "already_ended"} when the session or shell was not live -- for example, a shell that
closed on its own between listing and this call. Not an error either way.
Error Cases:
-
400-{"error":"cannot end the current session here — use logout"}--sidnames the caller's own current session -
404- the id does not belong to the caller
Audit session.revoked (reason revoked_self), or ssh.session_closed (detail reason=forced)
for a shell.
cURL:
curl -X DELETE https://hub.example.com/api/auth/sessions/SESSION_ID \
-H 'Authorization: Bearer <access_token>'End every one of the caller's sessions except the current one, and close every one of the caller's own open shells.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), interactive session only |
Response (200):
{"ended": 2, "shells_closed": 1}Audit one session.revoked (reason revoked_self) per ended session and one
ssh.session_closed (detail reason=forced) per closed shell.
cURL:
curl -X POST https://hub.example.com/api/auth/sessions/sign-out-others \
-H 'Authorization: Bearer <access_token>'Sign out the calling session. Unchanged request/response shape; feature 009 adds a side effect.
| Property | Value |
|---|---|
| Auth | Access token (Bearer) -- cookie or header |
Response: 204 No Content.
Clears the browser's auth cookies and blacklists the access token, exactly as before. As of
feature 009, it also ends the caller's server-side session (audit session.revoked, reason
logout) -- previously logout left any session record for the caller live, reachable only via the
blunter token-generation bump. vey logout relies on this: the CLI's server-side session is now
genuinely ended, not merely forgotten on disk (see CLI).
cURL:
curl -X POST https://hub.example.com/api/auth/logout \
-H 'Authorization: Bearer <access_token>'List a user's sessions.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID
Query Parameters:
-
include_ended- Whentrue, also return sessions ended in the last 30 days, each carryingended_atandend_reason
Response (200):
{
"sessions": [
{"id":"…","kind":"web","ip":"10.0.0.5","user_agent":"Mozilla/…","created_at":"…","last_seen_at":"…","expires_at":"…","idle_deadline_at":"…","current":false},
{"id":"…","kind":"cli","ip":"…","user_agent":"vey/2.0.37","created_at":"…","last_seen_at":"…","expires_at":"…","idle_deadline_at":"…","current":false},
{"id":"shell:srv-01:sess-9f2","kind":"ssh","server":"web-01","started_at":"…","last_activity_at":"…","current":false}
]
}Live rows first; a row is marked current: true only when it is the session the requesting admin
is themselves using right now (i.e. the target user is the caller). With include_ended=true,
ended rows from the last 30 days follow, each with end_reason one of revoked_admin |
revoked_self | revoked_disable | logout | expired_idle | expired_absolute. Ended sessions
older than 30 days are pruned and no longer appear here or anywhere else.
Error Cases:
-
404-{"error":"user not found"}
cURL:
curl https://hub.example.com/api/users/USER_UUID/sessions?include_ended=true \
-H 'Authorization: Bearer <access_token>'End one session, or one shell, belonging to a user.
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID -
sid- Session id, orshell:<serverID>:<sessionID>
Response (200):
{"status": "ended"}{"status": "already_ended"} when it was not live -- concurrent revocation by two admins is
idempotent this way, with exactly one audit event for the actual revocation.
Error Cases:
-
404-{"error":"user not found"}, or the session/shell id does not belong to that user
An administrator ending their own current session through this endpoint is allowed -- they are
signed out like anyone else; see Settings. Audit session.revoked (reason revoked_admin,
Target = the session's owner) or ssh.session_closed (detail reason=forced) for a shell.
cURL:
curl -X DELETE https://hub.example.com/api/users/USER_UUID/sessions/SESSION_ID \
-H 'Authorization: Bearer <access_token>'End every live session of a user and close every one of their open shells -- "Log out everywhere."
| Property | Value |
|---|---|
| Auth | Access token (Bearer), admin only |
Path Parameters:
-
id- Target user UUID
Response (200):
{"ended": 3, "shells_closed": 1}Error Cases:
-
404-{"error":"user not found"}
One session.revoked (reason revoked_admin) per session ended, and one ssh.session_closed
(detail reason=forced) per shell closed. The same server-side helper backs 008's PUT /api/users/{id}/status {"disabled":true}, which calls it with reason revoked_disable instead --
disabling an account now also ends every session and closes every shell it has open.
cURL:
curl -X DELETE https://hub.example.com/api/users/USER_UUID/sessions \
-H 'Authorization: Bearer <access_token>'Returns the verified public manifest for a build offered by this Hub. This
public route supports component=cli on Linux and macOS amd64/arm64, and
component=agent on Linux amd64/arm64. Local vey update uses the CLI
manifest and fixed binary path /install/cli/{os}/{arch}. The agent bootstrap
uses the matching agent manifest and /install/{os}/{arch} binary. Neither
route requires login. Update clients require HTTPS with a valid certificate
and refuse redirects.
{
"schema_version": 1,
"component": "cli",
"os": "linux",
"arch": "amd64",
"version": "<version>",
"revision": "<40-or-64-character-source-revision>",
"sha256": "<lowercase-sha256>",
"bytes": 12345678,
"path": "/install/cli/linux/amd64"
}The Hub serves metadata only when its versioned artifact manifest matches the binary bytes on disk. Development or mismatched distributions fail closed.
Returns the preserving Linux-agent bootstrap script. The script accepts the Hub
address and a short-lived, single-use ticket bound to an enrolled node and exact
Hub artifact. The bootstrap command printed by vey servers update NODE runs
the script over verified HTTPS with explicit sudo; the ticket is sent to the
running agent and is never part of the downloaded script. This public download
route does not grant update authority by itself.
All /api/servers/{id}/agent-update routes require a live interactive admin
session. API tokens are rejected. Check, start, and recovery mutation are
refused for installations that ever enrolled Enhanced or other governance
controls, as well as for nodes outside the supported Linux systemd deployment.
The enrolled node must already have a valid mTLS identity. The status route
remains available for read-only inspection of an existing durable operation.
The Hub chooses the exact artifact; clients cannot provide a URL or arbitrary
version.
Read-only capability and artifact check. The response includes the current
agent capabilities when available, the Hub's target artifact, whether an
update is available, whether a preserving bootstrap is required, and the latest
operation when present. For agents that cannot report file/log quiescence, it
also returns bootstrap_idle_confirmation_required: true and the exact current
connection_id. Native activity covers file list/read/delete, upload, and log
tail requests through actual completion. A request timeout or cancellation does
not prove that its agent work ended. Returns a conflict or forbidden error when
the node or installation is not eligible.
Starts one update after rechecking the current Hub artifact and administrator authority. The body accepts the exact checked version and source revision. The manual preserving-bootstrap path additionally accepts an operator idle assertion bound to the connection ID from the preceding check. For an original agent whose version and revision linker stamps are both absent, it also requires the operator's established original version and exact installed-byte SHA-256; these assertions must be supplied together:
Include both bootstrap_original_* fields below only for the unstamped-original
path; omit both for the ordinary preserving bootstrap.
{
"expected_version": "<version>",
"expected_revision": "<40-or-64-character-source-revision>",
"bootstrap_idle_confirmed": true,
"expected_connection_id": "<connection-id-from-check>",
"bootstrap_original_version": "<operator-established-original-version>",
"bootstrap_original_sha256": "<sha256-of-installed-agent>"
}For a supported agent, the Hub reserves the node for maintenance and dispatches the update. The Hub refuses while known or unconfirmed work is active and blocks new work once maintenance is reserved. For an older enrolled agent or one that cannot prove complete file/log quiescence, the operator must close file/log clients and sessions, allow work to finish, verify idle over direct SSH, and explicitly assert it. The Hub binds that assertion to the exact connection generation and still refuses known active work; it is not an automatic detector or a native quiescence override. The response then includes a node-bound, single-use bootstrap ticket with a 120-second expiry. The ticket is returned only in the start response; status responses never include it. For the unstamped-original path, the agent helper verifies the supported Linux package/platform, requires both linker-stamped identity fields to be absent, and checks the installed bytes against the asserted SHA-256 before replacement. It reports that assertion through the existing enrolled mTLS connection. The Hub binds the assertion to the authenticated actor and session, node, fresh connection, and one-use ticket, and requires the target release to be newer than the asserted original version. The CLI renders the preserving direct-SSH command with those same assertions. Do not log or reuse the ticket.
Returns the latest durable update operation for the node when operation_id is
omitted, or that exact operation when supplied. The status records success only
after a fresh mTLS connection reports the exact target version, revision, and
checksum and the agent durably confirms that result. An unknown legacy
rollback state requires direct-SSH verification and explicit administrator
acknowledgement; it is not reported as success.
Records an administrator's attestation after direct SSH confirms the original binary and service. It requires an unknown operation, the exact recorded previous checksum, an idle node, and the expiry plus 190 seconds.
{
"operation_id": "<operation-id>",
"verified_sha256": "<recorded-previous-sha256>"
}This endpoint records the human verification and releases the maintenance reservation. It does not remotely verify the agent binary.
Application access is configured and managed in Application Access. These endpoints are served on the portal Hub origin. Portal authentication and CSRF rules apply; administration routes require the live operations.admin capability. Paginated collections use limit and offset (default 50, maximum 100).
| Method and path | Access | Contract summary |
|---|---|---|
GET /api/applications |
Signed-in user | Authorized applications only: {applications:[{id,name,description,public_url,available,availability_reason}],total}. Backend configuration is omitted. |
POST /api/applications/{id}/launch |
Signed-in user | JSON {path} creates {launch_url,ticket,expires_at,path}. The browser posts the one-use ticket to the exact launch_url; do not place tickets in URLs or logs. |
GET /api/admin/applications |
operations.admin |
Paginated full application configuration. |
POST /api/admin/applications |
operations.admin |
Create from {name,slug,description,route,node_id?,backend_url,allowed_cidrs,ca_pem?,enabled}. |
GET, PUT, DELETE /api/admin/applications/{id} |
operations.admin |
Read, update with expected_version, or delete. Update version conflicts return 409. |
GET, PUT /api/admin/applications/{id}/grants |
operations.admin |
Read or replace {grants:[{kind,subject_id}],expected_version}; grant kinds are user, role, local_group, and directory_group. |
GET, POST /api/admin/application-groups |
operations.admin |
Paginated local groups (list rows include member_count, not user_ids) or create a group. |
GET, PUT, DELETE /api/admin/application-groups/{id} |
operations.admin |
Read, update {name,user_ids,expected_version}, or delete. |
GET, PUT /api/admin/application-settings |
operations.admin |
Read or update {idle_seconds,max_seconds,expected_version}. |
GET /api/admin/application-subjects/users?limit=50&offset=0&q=... |
operations.admin |
Bounded user selector page: {users:[{id,username}],total}. |
GET /api/admin/application-subjects/directory-groups?q=... |
operations.admin |
Directory subjects: {groups:[{subject_id,name}],total}. Send the returned stable subject_id unchanged. |
GET /api/auth/application-sessions?limit=50&offset=0 |
Signed-in user | Bounded page {sessions:[...],total} for this user, including application name, idle deadline, absolute expiry, and end reason when ended. |
DELETE /api/auth/application-sessions/{id} |
Session owner | Revoke one application session and cancel its streams. |
Application grants and configuration changes revoke affected sessions. Application sessions use their own idle and absolute deadlines, separate from portal sessions. A browser-only unauthenticated application document request can be redirected to the fixed portal launch flow; API and non-browser requests receive 401.
Governance request, approval, and applier-claim writes revalidate a live actor-owned session and the proof's credential generation inside the SQLite writer transaction. Proof consumption and mutation commit together. Expiry decisions advance the caller's clock through writer-lock waits; revoked or expired session, proof, or request authority is refused without consuming the proof. Password and TOTP completion prepare settings before sampling the mutation clock, then include connection and writer-lock waits in the transactional authority check.
Veyport User Guide
Getting Started
Using Veyport
Administration
Setup & Deployment
- Architecture
- Deployment
- Deployment On-Prem
- Deployment AWS
- Deployment Azure
- Deployment GCP
- Proxy Configuration
- Development
- Dependency Track
- API Reference
Help