REST API
In addition to the grpc-gateway proxy (which exposes all VigoAdmin RPCs under /api/v1/), the server mounts several direct REST handlers. These endpoints are not proxied through grpc-gateway and exist for features that benefit from direct HTTP handling.
All endpoints return JSON and accept JSON request bodies. Errors use the format {"message": "..."}.
Authorization
Every endpoint requires both authentication (session cookie or vgot_* API token) and a specific permission. Read endpoints (status, list, get) require fleet.read; mutation endpoints require a permission matched to the action — config.publish for publish/freeze/reload/retract, config.rollback for rollback, push.execute for task dispatch, workflow.execute for workflow run/abort, query.execute for live queries and the AI assistant, fleet.write for envoy maintenance and force-update, audit.read for the raw audit log. Database administration, peer promotion, secret-rotation callbacks, and emergency-access recording require the admin role. The compliance framework reports + audit-bundle download ride compliance.read; supporting-doc upload requires compliance.docs.write. See the Authentication doc for the full role/permission matrix.
Tasks
Ad-hoc command dispatch to connected agents.
POST /api/v1/task
Permission: push.execute
Dispatch a command to matching envoys.
Request body:
{
"command": "uptime",
"target": "*.web.*",
"operator": "admin",
"timeout": 30,
"nice": 0,
"batchSize": "25%",
"maxFailures": "3",
"healthCheck": "curl -sf http://localhost/health",
"dryRun": false,
"taskName": "",
"params": {},
"force": false
}
| Field | Required | Description |
|---|---|---|
command |
Yes (or taskName) |
Shell command to execute |
target |
Yes (or taskName) |
Hostname glob or trait filter (e.g., *.web.*, os.family=debian) |
operator |
No | Who initiated the task (default: api) |
timeout |
No | Max seconds per target (default: 60) |
nice |
No | CPU/IO priority 0–19 (0 = normal, 19 = lowest; default: 0) |
batchSize |
No | Rolling execution batch size (e.g., 25%, 5) |
maxFailures |
No | Abort after this many failures (e.g., 3, 10%) |
healthCheck |
No | Command to run between batches |
dryRun |
No | If true, return matched targets without executing |
taskName |
No | Name of a reusable task definition from .live/tasks/*.vgo |
params |
No | Parameter overrides for named tasks |
force |
No | If true, skip the server-side guardrail check (agent guardrails still apply; audited as task.dispatch with "forced":true payload) |
Response (201 Created):
{
"id": "abc123",
"status": "running",
"command": "uptime",
"targetGlob": "*.web.*",
"totalTargets": 12,
"targets": [...]
}
Response (400 Bad Request) — guardrail block:
When the command matches a hardcoded dangerous pattern and force is
false, the dispatch is refused with a structured body the CLI keys on
for its confirmation prompt:
{
"message": "command matches dangerous pattern: systemctl stop vigo",
"blocked": true,
"reason": "command matches dangerous pattern",
"pattern": "systemctl stop vigo"
}
Refused dispatches are recorded as task.blocked on the audit hash
chain.
GET /api/v1/task
Permission: fleet.read
List recent task runs.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
limit |
20 | Max results to return |
Response:
{
"tasks": [
{
"id": "abc123",
"command": "uptime",
"target": "*.web.*",
"operator": "admin",
"status": "complete",
"totalTargets": 12,
"createdAt": "2026-03-13T10:00:00Z",
"finishedAt": "2026-03-13T10:00:05Z"
}
]
}
GET /api/v1/task/{id}
Permission: fleet.read
Get a task run with per-target results.
Response:
{
"task": {
"id": "abc123",
"command": "uptime",
"target": "*.web.*",
"operator": "admin",
"status": "complete",
"totalTargets": 2,
"timeoutSecs": 60,
"createdAt": "2026-03-13T10:00:00Z",
"finishedAt": "2026-03-13T10:00:05Z"
},
"targets": [
{
"id": "t1",
"hostname": "web-01.example.com",
"status": "complete",
"exitCode": 0,
"stdout": " 10:00:05 up 42 days",
"durationMs": 120
}
]
}
POST /api/v1/task/{id}/cancel
Permission: push.execute
Cancel a specific task run. Sets the task and its pending/dispatched targets to cancelled status.
Response:
{"status": "cancelled", "id": "abc123"}
POST /api/v1/task/cancel-running
Permission: push.execute
Cancel all tasks stuck in running or pending state. Also cancels their pending/dispatched targets.
Response:
{"status": "cancelled", "count": 2}
Workflows
Multi-step task orchestration with conditional branching.
POST /api/v1/workflows
Permission: workflow.execute
Start a workflow execution. Recorded on the audit hash chain as
workflow.dispatch with the workflow name in the summary.
Request body:
{
"name": "deploy-web",
"operator": "admin"
}
Response (201 Created):
{
"id": "wf-abc123",
"status": "running"
}
GET /api/v1/workflows
Permission: fleet.read
List recent workflow runs.
Query parameters: limit (default 20)
Response:
{
"workflows": [
{
"id": "wf-abc123",
"workflowName": "deploy-web",
"status": "complete",
"currentStep": 3,
"totalSteps": 3,
"operator": "admin",
"createdAt": "2026-03-13T10:00:00Z"
}
]
}
GET /api/v1/workflows/{id}
Permission: fleet.read
Get a workflow run with per-step results.
POST /api/v1/workflows/{id}/abort
Permission: workflow.execute
Abort a running workflow. Returns {"status": "aborting"}.
Live Query
POST /api/v1/query
Permission: query.execute
Query trait values from connected agents in real time.
Request body:
{
"traitPath": "os.distro",
"target": "*.web.*",
"timeout": 10
}
| Field | Required | Description |
|---|---|---|
traitPath |
Yes | Dot-separated trait path (e.g., os.distro, hardware.memory_mb) |
target |
Yes | Hostname glob or trait filter |
timeout |
No | Seconds to wait for responses (default: 10) |
Response:
{
"responded": [
{"hostname": "web-01", "value": "ubuntu", "found": true}
],
"timedOut": [
{"hostname": "web-03"}
],
"offline": [
{"hostname": "web-05", "value": "ubuntu", "found": true, "collectedAt": "..."}
],
"counts": {"responded": 1, "timedOut": 1, "offline": 1}
}
AI Assistant
POST /api/v1/ask
Permission: query.execute
Ask the AI assistant a question about your fleet.
Request body:
{
"question": "Why is web-01.example.com failing?",
"context": {
"envoy": "web-01.example.com"
},
"history": [
{"role": "user", "content": "previous question"},
{"role": "assistant", "content": "previous answer"}
]
}
Response (JSON):
{
"answer": "web-01.example.com is failing because...",
"tools_used": ["get_envoy", "list_runs"],
"tokens_used": 1234
}
Response (SSE, if Accept: text/event-stream):
Events are sent as server-sent events with JSON payloads:
{"type": "status", "status": "thinking", "tools_called": ["get_envoy"]}{"type": "delta", "text": "The envoy is..."}{"type": "done", "tools_used": ["get_envoy"], "tokens_used": 1234}
Doctor
GET /api/v1/doctor
Permission: fleet.read
Health check endpoint that validates server subsystems.
Response:
{
"status": "healthy",
"checks": [
{"name": "database", "scope": "server", "status": "pass"},
{"name": "tls", "scope": "server", "status": "pass", "message": "expires 2027-03-13"},
{"name": "secrets", "scope": "server", "status": "pass", "message": "local"},
{"name": "license", "scope": "server", "status": "pass", "message": "compliant, 42/100 envoys"},
{"name": "spanner", "scope": "server", "status": "skip", "message": "standalone"},
{"name": "integrations", "scope": "server", "status": "skip", "message": "no integrations configured"},
{"name": "config", "scope": "server", "status": "pass", "message": "15 configcrates, 8 entries"},
{"name": "stale_envoys", "scope": "fleet", "status": "pass"}
]
}
Check statuses: pass, warn, fail, skip. Check scope: server (Vigo itself) or puddle (managed envoys). Overall status: healthy, degraded, unhealthy. The Vigo Health page renders only server-scope checks; puddle conditions surface on the dashboard and envoys views.
License
GET /api/v1/license
Permission: fleet.read
Returns current license status including stage, tier, envoy counts, and expiration.
Customer identity (customer_name, customer_id, license_id) and the machine fingerprint are admin-only — for viewer and compliance callers these fields are blanked, leaving the operational fields (tier, counts, expiry) intact.
Response:
{
"stage": 0,
"license": {
"customer_name": "Acme Corp",
"customer_id": "acme",
"license_id": "lic-20260313-acme",
"tier": "enterprise",
"max_envoys": 500,
"issued_at": "2026-03-13",
"expires_at": "2027-03-13",
"version": 1
},
"active_envoys": 12,
"max_envoys": 500,
"expires_at": "2027-03-13",
"days_remaining": 365
}
The tier field is "free", "standard", "growth", or "enterprise". Omitted for legacy licenses without a tier (treated as free).
Traits
GET /api/v1/envoys/{hostname}/traits
Permission: fleet.read
Returns the latest collected traits for a envoy by hostname.
Response:
{
"hostname": "web-01.example.com",
"collected_at": "2026-03-13T10:00:00Z",
"traits": {
"os": {"distro": "ubuntu", "version": "24.04"},
"hardware": {"memory_mb": 8192, "cpu_count": 4}
}
}
Inventory
GET /api/v1/inventory
Permission: fleet.read
Query envoys by trait values.
Query parameters:
| Parameter | Description |
|---|---|
where |
Repeatable. Each is dot.path=value (e.g., where=os.distro=ubuntu) |
show |
Comma-separated dot-paths to include in output (default: all traits) |
Response:
{
"count": 5,
"envoys": [
{
"hostname": "web-01.example.com",
"ip_address": "10.0.1.10",
"collected_at": "2026-03-13T10:00:00Z",
"traits": {"os.distro": "ubuntu"}
}
]
}
Spanner
Internally still implemented as hub-spoke (see
server/spanner/); the REST surface speaks the peer-equal redesign's spanner/bolt vocabulary.
GET /api/v1/spanner/status
Permission: fleet.read
Returns spanner role and bolt statuses (hub mode only).
POST /api/v1/spanner/init
Permission: Admin role
Founds the spanner on this server: writes the founder self-admit row into the admissions roster, signed with this server's bolt identity. Requires spanner.spanner_id set in server.yaml. Refuses with 409 if this server already holds a roster row. Driven by vigocli spanner init.
POST /api/v1/spanner/bolt/invite
Permission: Admin role
Mints and signs a single-use invite bundle authorizing another server to join this spanner. The bundle carries a random token, the spanner ID, this server's peer address, and an expiry, all signed with the local bolt identity. Driven by vigocli spanner bolt invite.
Request body:
{
"ttl_seconds": 86400
}
POST /api/v1/spanner/bolt/admit
Permission: Invite-bundle authenticated (not Bearer-gated — this is the server-to-server call a joining bolt makes).
Admits a joining bolt. Verifies the invite bundle's signature + freshness, rejects a reused token (the token may be consumed exactly once), then in one transaction consumes the token and inserts the joining bolt's signed admission row. Returns the joiner's signed row plus the full current roster and its consumed-token rows.
Request body:
{
"joiner_pubkey": "...",
"hostname": "joiner.example.com",
"pattern_claims": ["*.us-east.*"],
"bundle": "..."
}
POST /api/v1/spanner/bolt/join
Permission: Admin role
Joins an existing spanner. Run on the joining server: forwards this server's identity + the invite bundle to the inviting server's /api/v1/spanner/bolt/admit endpoint, then persists the returned roster into the local admissions table. Driven by vigocli spanner bolt join --from <url> --code <bundle>.
Request body:
{
"from_addr": "https://srv1.example.com:8443",
"bundle": "..."
}
GET /api/v1/spanner/roster
Permission: fleet.read
Returns the full admissions roster (every spanner_bolts row) so the membership ceremony's result is inspectable. Backs vigocli spanner bolt list.
GET /api/v1/spanner/snapshot
Permission: fleet.read
Serves this server's full admissions roster + consumed-token G-Set as a signed CRDT snapshot. Peer bolts' roster gossipers pull this endpoint on the Tier-2 interval (spanner.snapshot.tier2_interval, default 5m) and CRDT-merge the result, so every bolt converges on one admissions roster. Each row carries its original Ed25519 admission signature; the pulling peer verifies every row before merging. Server-to-server, not operator-driven.
GET /api/v1/spanner/fleet-snapshot
Permission: fleet.read
Serves this bolt's fleet-observability snapshot — its envoy roster plus convergence counters — signed with the bolt's Ed25519 identity. Peer bolts pull this on the same Tier-2 cadence as /snapshot and verify the signature against the publishing bolt's roster pubkey before caching. The cache backs the peer-equal fleet-wide admin reads (ListEnvoys, GetConvergenceSummary) when spanner.fallback_admin: local. Kept separate from /snapshot — different consistency model (per-bolt facts, last-writer-wins) and size profile. Server-to-server, not operator-driven.
GET /api/v1/spanner/bolts/{id}
Permission: fleet.read
Returns detailed status for a single bolt.
GET /api/v1/spanner/bolts/{id}/envoys
Permission: fleet.read
List envoys on a specific bolt.
GET /api/v1/spanner/bolts/{id}/runs
Permission: fleet.read
List recent runs on a specific bolt.
POST /api/v1/spanner/bolts/{id}/push
Permission: Admin role
Force-push to envoys on a specific bolt.
POST /api/v1/spanner/reassign
Permission: Admin role
Reassign an envoy from one bolt to another.
Request body:
{
"envoy_id": "...",
"from_bolt_id": "east-1",
"to_bolt_id": "west-1"
}
POST /api/v1/spanner/bolts/{id}/drain
Permission: Admin role
Drain all envoys from a bolt (move to target bolt or auto-route by patterns).
POST /api/v1/spanner/ops-puddle/pair-claim
Permission: Admin role
Claim a pair offer minted by vigocli spanner ops-puddle pair on this vigosrv. Validates the pair code against the on-disk pending-pair record, marks it used, appends the joining vigosrv's pubkey to the ops-puddle member list, and returns the raw 32-byte Ed25519 seed (base64) + friendly name + updated members list so the joining vigosrv can re-wrap the seed under its own master-key-derived wrap key.
Called by vigocli spanner ops-puddle join --from <this-vigosrv> --code <code> on the joining vigosrv. mTLS protects the seed in transit; the pair code is the authorization factor and is single-use with a 5-minute TTL.
Request body:
{
"code": "8h3K_z2pYvR4qLwx",
"joining_pubkey": "ed25519:f81de2c4a8f1bb4d6e23b07c5b920fd1a2eb43d9a09f3e2b40c47ab9a83b66c1"
}
Response:
{
"seed_b64": "<base64 of raw 32-byte Ed25519 seed>",
"friendly_name": "alexander4-ops",
"members": [
{"pubkey": "ed25519:...", "joined_at": "2026-05-17T21:55:03Z"},
{"pubkey": "ed25519:...", "joined_at": "2026-05-17T22:01:14Z"}
]
}
Errors: 404 (no pending offer), 410 (code expired or already used), 403 (code mismatch), 503 (secrets backend doesn't support HKDF — only the local backend does in v1).
Emits the spanner.ops_puddle.member_added audit event on success.
Config
GET /api/v1/config/trace
Permission: fleet.read
Two-direction config trace.
Forward (hostname-rooted) — trace config resolution for one host. Shows the full directory inheritance chain with vars and override markers at each level, which configcrates came from which files or roles, how roles expanded, and where each variable's final value originated.
Inverse (subject-rooted) — given a ?configcrate= or ?usercrate= without a ?hostname=, returns every enrolled envoy whose resolved config includes that subject, plus the carrier (file path / role / environment) that pulled it in. With ?include_tree=true, additionally lists config-tree patterns that reference the subject but match no enrolled envoy. The response shape is InverseTraceResult (below) — distinct from the forward-mode ConfigTrace.
Query parameters:
| Parameter | Description |
|---|---|
hostname |
Forward mode: the hostname to trace. |
configcrate |
With hostname: filter the forward trace to this configcrate's branch. Without hostname: inverse subject — find where this configcrate is applied. |
usercrate |
Inverse subject (cannot be combined with hostname or configcrate) — find where this usercrate is applied. |
include_tree |
Inverse mode only. true widens the result to include config-tree patterns no enrolled envoy currently matches. |
Exactly one subject must be specified: hostname, configcrate alone, or usercrate. Combining configcrate and usercrate is rejected with 400.
Inverse response (InverseTraceResult):
{
"subject": "configcrate",
"value": "nginx",
"include_tree": false,
"envoys": [
{
"envoy_id": "envoy-abc123",
"hostname": "web01.prod.example.com",
"match": "web*.prod.example.com",
"environment": "production",
"carrier": "production/common.vgo"
}
],
"tree_refs": [
{
"match": "staging-only.example.com",
"environment": "staging",
"carrier": "staging/common.vgo"
}
]
}
tree_refs is omitted unless include_tree=true was requested.
Forward response (ConfigTrace):
Response:
{
"hostname": "web01.prod.example.com",
"matched": true,
"match_pattern": "web*.prod.example.com",
"pattern_type": "glob",
"environment": "production",
"roles": ["webserver"],
"role_expansion": [
{
"role": "webserver",
"includes": ["base-security"],
"configcrates": ["firewall", "sshd", "nginx", "logrotate"]
}
],
"inheritance": [
{
"dir": "",
"file": "common.vgo",
"configcrates": ["monitoring"],
"vars": {"dns_server": "1.1.1.1"}
},
{
"dir": "production",
"file": "common.vgo",
"configcrates": ["log-shipping"],
"vars": {"dns_server": "10.0.0.1", "log_level": "warn"},
"overrides": ["dns_server"]
}
],
"configcrates": ["monitoring", "log-shipping", "firewall", "sshd", "nginx", "logrotate"],
"configcrate_sources": {
"monitoring": "common.vgo",
"log-shipping": "production/common.vgo",
"firewall": "role:webserver",
"sshd": "role:webserver",
"nginx": "role:webserver",
"logrotate": "role:webserver"
},
"var_sources": [
{"key": "nginx_port", "value": 443, "source": "environments.vgo:production"},
{"key": "log_level", "value": "warn", "source": "match-block"}
],
"resource_count": 14
}
When ?configcrate= is provided, the inheritance, configcrates, and role_expansion fields are filtered to only entries relevant to that configcrate. Vars are kept unfiltered.
If no entry matches, matched is false and other fields are omitted.
GET /api/v1/config/tree
Permission: fleet.read
Returns the directory inheritance structure for visualization. Shows common.vgo files at each level, leaf entries with their match patterns, and accumulated configcrate totals.
Response: A tree of nodes with dir, file, configcrates, roles, match, total, excluded, and children fields.
GET /api/v1/config/search
Permission: fleet.read
Search envoy entries by match pattern, configcrate name, or var name. Returns matching entries with their full inheritance chain and accumulated configcrates.
Query parameters:
| Parameter | Required | Description |
|---|---|---|
match |
No | Glob pattern to match against envoy match: patterns (e.g., *web*) |
configcrate |
No | Configcrate name — find entries that use this configcrate (inherited or direct) |
var |
No | Var name — find entries that define this var |
At least one parameter is required.
Response:
{
"entries": [
{
"match": "web*.prod.example.com",
"dir": "production/web",
"inheritance": [
{"dir": "", "file": "common.vgo", "configcrates": ["sshd", "monitoring"]},
{"dir": "production", "file": "common.vgo", "configcrates": ["log-shipping"]}
],
"own_configcrates": ["nginx"],
"total": ["sshd", "monitoring", "log-shipping", "nginx"],
"vars": {"log_level": "warn", "nginx_port": 443}
}
]
}
Audit
GET /api/v1/audit
Permission: audit.read
Unified audit log across config publishes, task runs, and workflows.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
since |
— | Time filter: duration (24h) or RFC3339 timestamp |
operator |
— | Filter by operator name |
type |
— | Event type: config_publish, task_run, workflow_run |
limit |
50 |
Maximum events (max 200) |
Response:
{
"events": [
{
"id": "a1b2c3d4",
"type": "config_publish",
"operator": "admin",
"timestamp": "2026-03-13T14:30:00Z",
"summary": "3 files changed"
}
]
}
GET /api/v1/audit/verify
Permission: audit.read
Verifies the SHA-256 hash chain integrity of the audit log.
Response:
{
"valid": true,
"entry_count": 1234,
"first_broken": null,
"error": ""
}
If the chain is broken:
{
"valid": false,
"entry_count": 1234,
"first_broken": 567,
"error": "hash mismatch at entry 567: expected a1b2c3d4... got e5f6a7b8..."
}
POST /api/v1/emergency-access
Permission: Admin role
Record an emergency access event in the tamper-evident audit chain (HIPAA 164.312(a)(2)(ii)). Used by vigocli auth emergency-access for break-glass scenarios.
Request body:
{
"reason": "Account lockout — resetting credentials for operator jsmith"
}
| Field | Required | Description |
|---|---|---|
reason |
Yes | Free-text justification for the emergency access |
Response (200 OK):
{
"status": "recorded"
}
Config Publishes
GET /api/v1/config/publishes
Permission: fleet.read
List recent config publish records.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
limit |
20 |
Maximum records (max 100) |
POST /api/v1/config/publishes
Permission: config.publish
Record a config publish for audit trail.
Request body:
{
"operator": "admin",
"changed_files": ["configcrates/nginx.vgo"],
"removed_files": [],
"diff_summary": "--- configcrates/nginx.vgo\n+++ configcrates/nginx.vgo\n..."
}
GET /api/v1/config/publishes/{id}
Permission: fleet.read
Get details for a specific config publish, including the diff summary.
Config Versions and Rollback
GET /api/v1/config/versions
Permission: fleet.read
List available config snapshots for rollback.
Response:
{
"versions": ["20260313T143000Z", "20260312T091500Z"]
}
POST /api/v1/config/rollback/{version}
Permission: config.rollback
Rollback to a previous config snapshot.
Config Freeze
POST /api/v1/config/freeze
Permission: config.publish
Set a global config publish freeze.
Request body:
{
"freeze_until": "2026-03-14T06:00:00Z"
}
POST /api/v1/config/unfreeze
Permission: config.publish
Remove the config publish freeze.
GET /api/v1/config/status
Permission: fleet.read
Reload health for the server's config store. Returns healthy: false when the last loadAll (startup or Reload) failed with an IdempotencyErrors — in that case the store started empty and operators would otherwise see a silently-empty config with no signal.
Response:
{
"healthy": true,
"version": 7,
"last_error": "",
"last_reload_at": "2026-05-24T18:32:14Z"
}
version increments on every successful reload. last_error is the formatted error message (empty when healthy: true). last_reload_at is RFC3339 UTC; empty only if loadAll has never run (the store was constructed but errored before its first load attempt).
POST /api/v1/config/reload
Permission: config.publish
Re-read all config files from disk. Called automatically by vigocli config publish.
POST /api/v1/config/retract
Permission: config.publish
Generate retract configcrates for removed configcrates. Called automatically by vigocli config publish when publish.retraction.enabled and ai.enabled are both true. Can also be called directly.
Request body:
{
"configcrates": [
{"name": "nginx", "content": "name: nginx\nresources:\n - name: install\n type: package\n package: nginx"}
]
}
Response:
{
"enabled": true,
"results": [
{
"name": "nginx",
"retract_yaml": "name: nginx-retract\n...",
"warnings": [{"resource": "install", "type": "package", "confidence": "high", "message": "remove nginx"}],
"envoys": [{"hostname": "web1.example.com", "last_run": "2026-04-01T12:00:00Z"}]
}
]
}
When retraction or AI is disabled, returns {"enabled": false, "reason": "..."}. See Configcrate Retraction.
POST /api/v1/secrets/rotated
Permission: Admin role (internal — called by the watcher subsystem)
Notify the server that a secret has been changed. Triggers watch_secret resources on affected envoys at their next check-in. Called automatically by vigocli secrets set.
Request body:
{
"path": "vigo/grafana/admin-password"
}
Response:
{
"path": "vigo/grafana/admin-password",
"affected": 2
}
POST /api/v1/secrets/accessed
Permission: Admin role (internal — called by the watcher subsystem)
Record an audit trail entry when a secret is revealed via CLI. Called automatically by vigocli secrets reveal.
Request body:
{
"path": "vigo/db/dsn"
}
Response:
{
"recorded": true
}
The event is recorded in the tamper-evident audit chain as type secret_accessed.
POST /api/v1/secrets/event
Permission: Admin role (internal — called by the vigocli unlock-gate lifecycle verbs)
Record an audit trail entry for an unlock-gate lifecycle action. Called automatically by vigocli secrets unlock / lock / rotate / reset.
Request body:
{
"type": "secrets.unlocked"
}
The accepted event types are:
| Type | Source verb |
|---|---|
secrets_unlocked |
vigocli secrets unlock |
secrets_locked |
vigocli secrets lock |
secrets_passphrase_rotated |
vigocli secrets rotate |
secrets_passphrase_reset |
vigocli secrets reset |
Any other type value is rejected with 400 so the audit chain can't be polluted by client-supplied event names.
Response:
{
"recorded": true
}
Envoys
GET /api/v1/envoys
Permission: fleet.read (rides the gRPC gateway; gate enforced by the gRPC interceptor)
Paginated list of envoys. Default page size is 500; the server clamps any limit above 5000 down to 5000 so the JSON response stays under grpc-gateway's 4 MB max-message limit. To walk the full fleet, iterate using the nextOffset from each response until hasMore: false.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
includeRevoked |
false |
If true, include revoked envoys in the response. |
limit |
500 |
Page size (1–5000; values outside this range are clamped). |
offset |
0 |
0-based pagination offset. |
Response:
{
"envoys": [
{
"id": "2e66cb50-4e75-4859-bf24-c16588dd4f3e",
"hostname": "web01.example.com",
"ipAddress": "10.0.0.10",
"enrolledAt": "2026-04-01T12:00:00Z",
"lastSeen": "2026-05-25T09:42:11Z",
"revoked": false,
"agentVersion": "0.68.4"
}
],
"total": 12500,
"offset": 0,
"limit": 500,
"hasMore": true,
"nextOffset": 500
}
Walking the full fleet (curl example):
offset=0
while :; do
page=$(curl -sk -H "Authorization: Bearer $TOKEN" \
"https://vigo:8443/api/v1/envoys?limit=5000&offset=$offset")
echo "$page" | jq -r '.envoys[].hostname'
[ "$(echo "$page" | jq -r '.hasMore')" = "false" ] && break
offset=$(echo "$page" | jq -r '.nextOffset')
done
vigocli envoys list already walks pages automatically.
Envoy Tags
PUT /api/v1/envoys/{id}/tags
Permission: fleet.write (rides the gRPC gateway; gate enforced by the gRPC interceptor)
Set tags on a envoy.
Request body:
{
"tags": ["webserver", "production"]
}
GET /api/v1/envoys/{id}/tags
Permission: fleet.read
Get tags for a envoy.
Envoy Maintenance
PUT /api/v1/envoys/{id}/maintenance
Permission: fleet.write
Set a maintenance window for a envoy.
Request body:
{
"until": "2026-03-14T06:00:00Z"
}
DELETE /api/v1/envoys/{id}/maintenance
Permission: fleet.write
Clear the maintenance window.
Envoy Compliance
GET /api/v1/envoys/{id}/compliance
Permission: Authenticated (route-level gate pending — compliance.read recommended)
Returns the current compliance status for a envoy.
Response:
{
"envoy_id": "a1b2c3d4",
"hostname": "web-01.example.com",
"status": "compliant",
"last_run_at": "2026-03-16T10:00:00Z"
}
Compliance Waivers
Waivers — operator-recorded exceptions to a regulatory framework control (typically for documented-scope controls satisfied through process or external attestation rather than automated enforcement) — are config, not an API surface. Declare them in waivers.vgo and publish with vigocli config publish, the single config path. A waiver promotes a not-satisfied control to waived in the compliance report (never overriding an already-satisfied control) and surfaces on /compliance/waivers. There are no waiver create/delete endpoints; waiver changes are audited through the config-publish audit trail.
Compliance Documentation
Operator-uploaded artifacts (BAA, IR plan, risk assessment, training records, signed contracts, etc.) that satisfy documented-scope framework controls. Files are envelope-encrypted at rest with the secrets master key under compliance.docs.docs_dir (default /srv/vigo/compliance-docs/); metadata is in compliance_docs and link rows in compliance_doc_links.
Available only when the deployment uses the local secrets backend — under any other backend the routes return 503 Service Unavailable. Uploaded by permission: compliance.docs.write to upload/replace/soft-delete and to manage links; compliance.read to list/download; compliance.docs.purge to physically remove a soft-deleted doc and its ciphertext.
Every mutation records an entry in the tamper-evident audit chain with the doc's SHA-256.
POST /api/v1/compliance/docs
Permission: compliance.docs.write
Upload a new artifact.
Content-Type: multipart/form-data
| Field | Required | Description |
|---|---|---|
file |
Yes | The binary upload. Magic-byte detected MIME must match the configured allowlist (PDF, DOCX, XLSX, PPTX, TXT, MD, PNG, JPG, SVG by default). |
filename |
No | Override the multipart Filename. Defaults to the upload's filename when omitted. |
mime_type |
No | Declared MIME type. Validated against magic-byte detection — a renamed .pdf containing JPEG bytes is rejected. |
doc_type |
No | Free-form category for cross-framework reuse hints (BAA, IR Plan, Risk Assessment, Security Policy, Training Records, Pentest Report, etc.). |
links |
No | JSON array of {"framework": "...", "control_id": "..."} pairs. The doc is linked to each (framework, control_id) on creation. May be empty; manage links later via the link endpoints. |
Size caps come from compliance.docs.{max_doc_bytes, max_per_framework_bytes, max_total_bytes} in server.yaml. Defaults: 50 MiB per doc, 5 GiB per framework, 50 GiB total.
Response (201): doc metadata.
{
"id": "8f3a...",
"doc_type": "BAA",
"filename": "acme-baa.pdf",
"mime_type": "application/pdf",
"size_bytes": 142133,
"sha256": "9b71c1...",
"uploaded_by": "alice",
"uploaded_at": "2026-04-27T21:25:00-07:00"
}
GET /api/v1/compliance/docs
Permission: compliance.read
List active (non-soft-deleted) doc metadata. Optional filter by ?framework=&control_id= to narrow to docs attached to a specific control.
Response: {"docs": [{...}, {...}]}
GET /api/v1/compliance/docs/{id}
Permission: compliance.read
Single-doc metadata. Soft-deleted docs are still readable here so audit history remains queryable.
GET /api/v1/compliance/docs/{id}/download
Permission: compliance.read
Stream the decrypted doc. Sets Content-Type from the stored mime_type, Content-Disposition: attachment; filename=..., and X-Compliance-Doc-Sha256 for integrity verification at the client.
PUT /api/v1/compliance/docs/{id}
Permission: compliance.docs.write
Replace an existing doc. Same multipart shape as POST. The new doc inherits the predecessor's (framework, control_id) link set unless links is provided. The predecessor stays in place with replaced_at + replaced_by_id populated so audit history points to the new revision.
DELETE /api/v1/compliance/docs/{id}
Permission: compliance.docs.write
Soft-delete: hides the doc from list/by-control views but keeps the row + ciphertext on disk so historical audit bundles can still verify it. Returns 409 Conflict if already soft-deleted.
POST /api/v1/compliance/docs/{id}/purge
Permission: compliance.docs.purge
Physical purge: removes ciphertext from disk and the metadata row from the database. Cascades through compliance_doc_links. Reserved for the compliance.docs.purge permission.
GET /api/v1/compliance/docs/{id}/links
Permission: compliance.read
List every (framework, control_id) pair the doc is attached to.
Response:
{
"links": [
{"framework": "hipaa", "control_id": "164.308(b)(1)", "linked_at": "...", "linked_by": "alice"},
{"framework": "hitrust", "control_id": "09.f", "linked_at": "...", "linked_by": "alice"},
{"framework": "soc2", "control_id": "CC9.2", "linked_at": "...", "linked_by": "alice"}
]
}
POST /api/v1/compliance/docs/{id}/links
Permission: compliance.docs.write
Attach an existing doc to an additional (framework, control_id). Idempotent — duplicate links are no-ops.
Request body:
{ "framework": "soc2", "control_id": "CC9.2" }
DELETE /api/v1/compliance/docs/{id}/links/{framework}/{controlID}
Permission: compliance.docs.write
Detach a doc from a single (framework, control_id) without touching the underlying file. The doc may still be linked to other controls.
Audit Bundle Export
Self-contained zip suitable for handing to an external auditor. Bundles a single framework's compliance report (HTML + PDF), structured per-control evidence, the raw audit-chain entries for the period, daily fleet convergence history, and decrypted copies of every operator-uploaded compliance document attached to the framework's controls.
Permission gate: audit.read is mandatory; compliance.read is checked separately and gates inclusion of the docs/ directory + the documents block in evidence.json. A caller with audit.read but not compliance.read still receives a usable bundle minus the artifacts.
The download itself records a tamper-evident audit-chain entry (compliance.bundle.download).
GET /api/v1/report/audit-bundle/{slug}
Permission: compliance.read
Stream the zip.
| Query param | Default | Description |
|---|---|---|
period |
365d |
Lookback window for audit-log.csv and convergence.csv. Accepts integer days (365, 30), Nd, Nmo, or Ny. |
Response: Content-Type: application/zip, attachment with filename vigo-audit-bundle-<slug>-<YYYYMMDD>.zip.
Bundle layout:
| Path | Description |
|---|---|
manifest.json |
Framework, period, generated-at, list of artifacts with their plaintext SHA-256 digests. Auditors verify integrity by extracting the zip and sha256sum-ing each path. |
report.pdf |
WeasyPrint-rendered compliance report. Omitted with a warnings entry in manifest.json if the container lacks weasyprint. |
report.html |
Same content as text/html, always present so the bundle is readable without a PDF viewer. |
evidence.json |
Per-control structured evidence: scope, status, configcrates, traits, attached doc IDs + filenames + SHA-256 (when compliance.read is held). |
docs/<doc-id>__<filename> |
Decrypted copies of every active compliance doc linked to the framework's controls. Filename sanitized — path separators stripped so docs/ is flat. Only present with compliance.read. |
audit-log.csv |
Audit-chain entries with timestamps within the period. Columns: id, timestamp, event_type, actor, source, summary, payload, prev_hash, hash. |
convergence.csv |
Daily fleet convergence snapshots within the period. Columns: recorded_at, total, converged, degraded, failed, no_data, changed, diverged, offline. |
Verification example:
unzip vigo-audit-bundle-hipaa-20260427.zip -d audit/
cd audit
jq -r '.artifacts[] | "\(.sha256) \(.path)"' manifest.json | sha256sum -c
Every line should print <path>: OK. A mismatch indicates the bundle was tampered with after generation, the bundle was assembled across two different config publishes, or a doc was replaced/purged between zip-write attempts.
Envoy Force Update
POST /api/v1/envoys/{id}/update
Permission: fleet.write
Force an immediate check-in for a specific envoy. The envoy will receive the update at its next poll cycle.
POST /api/v1/envoys/update
Permission: fleet.write
Force an immediate check-in for all envoys.
Users
User account management. Requires admin authentication (except first-user bootstrap).
GET /api/v1/users
Permission: Admin role
List all user accounts. Admin only.
Response:
{
"users": [
{
"id": "u1",
"username": "admin",
"role": "admin",
"auth_method": "basic",
"created_at": "2026-03-01T00:00:00Z"
}
]
}
POST /api/v1/users
Permission: Admin role (open until the first admin exists, then admin-gated)
Create a new user account. The first user created is automatically assigned the admin role regardless of the request.
Request body:
{
"username": "ops-lead",
"password": "...",
"role": "admin"
}
PUT /api/v1/users/{id}
Permission: Admin role
Update a user account. Admin only. Each field (role, display_name, email, ssh_public_key) is a partial update — an omitted field preserves the stored value. Changing role recalculates permissions and writes a user.role_change audit entry; promotion to admin is rejected unless the username maps to a human OS user on the server host.
DELETE /api/v1/users/{id}
Permission: Admin role
Delete a user account. Admin only. Cannot delete the last admin.
POST /api/v1/users/totp/setup
Permission: Authenticated (own account)
Generate a TOTP secret for the authenticated user. Returns the otpauth:// URL for QR code scanning. TOTP is not yet enabled until confirmed via /totp/confirm.
Response (200 OK):
{
"url": "otpauth://totp/Vigo:admin?secret=JBSWY3DPEHPK3PXP&issuer=Vigo"
}
POST /api/v1/users/totp/confirm
Permission: Authenticated (own account)
Confirm TOTP setup by providing a valid 6-digit code from the authenticator app. This enables TOTP for the user and returns one-time recovery codes.
Request body:
{
"code": "123456"
}
Response (200 OK):
{
"enabled": true,
"recovery_codes": ["abc123", "def456", "..."]
}
POST /api/v1/users/totp/disable
Permission: Authenticated (own account)
Disable TOTP for the authenticated user. Requires a valid TOTP code or recovery code.
Request body:
{
"code": "123456"
}
Response (200 OK):
{
"disabled": true
}
API Tokens
Create and manage API tokens for programmatic access. Tokens use the vgot_ prefix.
POST /api/v1/auth/tokens
Permission: Authenticated (own account)
Create a new API token. Requires authentication. The token is owned by a web user and inherits that user's role at validation time.
Request body:
{
"name": "ci-pipeline",
"expires_in": "720h",
"user": "dan"
}
user names the owning web user. It is optional and defaults to the caller; it may only be set by the local-admin principal (the server-host CLI), which has no web-user row of its own and must therefore name an owner. A request authenticated as a regular web user that sets user to anyone else is rejected 403. A request whose resolved owner is not a real web user is rejected 400.
Response (201 Created):
{
"id": "tok-abc123",
"name": "ci-pipeline",
"token": "vgot_...",
"expires_at": "2026-04-15T10:00:00Z"
}
The plaintext token is only returned once at creation time.
GET /api/v1/auth/tokens
Permission: Authenticated (own account)
List API tokens for the authenticated user. Token values are not returned.
DELETE /api/v1/auth/tokens/{id}
Permission: Authenticated (own account)
Revoke an API token.
Preview
GET /api/v1/preview
Permission: fleet.read
Multi-node impact analysis showing which envoys would be affected by changes to a configcrate.
Query parameters:
| Parameter | Required | Description |
|---|---|---|
configcrate |
Yes | Configcrate name to analyze |
Response:
{
"configcrate": "nginx",
"affected": [
{"hostname": "web-01.example.com", "envoy_id": "a1b2c3d4"},
{"hostname": "web-02.example.com", "envoy_id": "e5f6g7h8"}
],
"count": 2
}
OpenAPI
GET /api/v1/openapi.json
Permission: Authenticated (any role)
Returns the OpenAPI specification as JSON.
Health Check
GET /healthz
Permission: Public (no authentication)
Returns a simple health status. Useful for load balancer probes and monitoring.
Response:
{"status":"ok","bolt_id":""}
The bolt_id field is populated when the server runs in spoke mode (i.e. this vigosrv is one bolt in a spanner).
Database Administration
These endpoints provide direct database inspection and maintenance. All endpoints require admin authentication.
GET /api/v1/db/stats
Permission: Admin role
Returns database statistics including file size, WAL size, page count, page size, and per-table row counts.
Response:
{
"driver": "sqlite",
"file_size": 4218924,
"wal_size": 32768,
"page_count": 1024,
"page_size": 4096,
"tables": {
"envoys": 42,
"runs": 1250,
"envoy_traits": 42
}
}
GET /api/v1/db/integrity
Permission: Admin role
Runs PRAGMA integrity_check and PRAGMA foreign_key_check (SQLite) or a connection ping (other drivers).
Response:
{
"driver": "sqlite",
"status": "ok",
"ok": true,
"foreign_key_violations": 0
}
If foreign key violations are found, a foreign_key_details array is included.
POST /api/v1/db/vacuum
Permission: Admin role
Runs VACUUM to reclaim unused space. SQLite only. Returns the database file size before and after.
Response:
{
"status": "ok",
"before": 4218924,
"after": 3145728,
"reclaimed": 1073196
}
POST /api/v1/db/checkpoint
Permission: Admin role
Forces a WAL checkpoint with TRUNCATE mode. SQLite only.
Response:
{
"status": "ok",
"pages_total": 128,
"pages_written": 128
}
POST /api/v1/db/query
Permission: Admin role
Execute a read-only SQL query. Write operations (INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, ATTACH, DETACH, REINDEX, VACUUM, PRAGMA) are rejected with 403.
Request body:
{
"sql": "SELECT hostname, ip_address FROM envoys WHERE status = 'active' LIMIT 10"
}
Response:
{
"columns": ["hostname", "ip_address"],
"rows": [
{"hostname": "web-01.example.com", "ip_address": "10.0.1.10"}
],
"count": 1
}
Other Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /metrics |
Prometheus metrics (see Metrics) |
| GET | /healthz |
Health check (see above) |
| GET | /bootstrap |
Agent bootstrap script |
| GET | /bootstrap/agent/{os}/{arch} |
Agent binary download |
| GET | /bootstrap/ca.pem |
CA certificate download |
| GET | /api/v1/agent/latest |
Agent version, SHA256 checksum, and ED25519 signature (?os=&arch=) |
| GET | /api/v1/agent/download/{os}/{arch} |
Agent binary download |
| GET | /api/v1/agent/signing-key |
ED25519 signing public key (hex) |
| POST | /api/v1/bootstrap/register |
Agent enrollment with CSR signing |
| GET | /api/v1/fleet/pubkeys |
Hostname → Ed25519 pubkey map for the (non-revoked) fleet. Used by agents to bootstrap their gitback peer-pubkey TOFU cache. Pubkeys are public-by-construction (broadcast on multicast); endpoint is unauthenticated. |
Export
Compliance evidence endpoints for integration with SIEM, CMDB, and OSCAL tools. Each endpoint must be individually enabled in server.yaml under the export: section. Disabled endpoints return 404.
GET /api/v1/export/siem
Permission: fleet.read
Returns a JSON array of SIEM events derived from run results.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
since |
24 hours ago | RFC 3339 timestamp. Only events after this time are returned |
envoy |
(all) | Filter by hostname |
limit |
1000 | Maximum events to return (max 10000) |
Response:
[
{
"timestamp": "2026-03-13T10:05:00Z",
"hostname": "web-01.example.com",
"envoy_id": "a1b2c3d4",
"run_id": "run-5678",
"configcrate_name": "nginx",
"resource_name": "nginx.conf",
"resource_type": "file",
"action": "updated",
"changed": true,
"duration_ms": 42,
"run_status": "compliant",
"source": "vigo"
}
]
| Field | Type | Description |
|---|---|---|
timestamp |
string | RFC 3339 time of the resource action |
hostname |
string | Envoy hostname |
envoy_id |
string | Envoy UUID |
run_id |
string | Convergence run ID |
configcrate_name |
string | Configcrate that owns the resource |
resource_name |
string | Resource name within the configcrate |
resource_type |
string | Resource type (file, package, service, etc.) |
action |
string | Action taken (created, updated, removed, noop, failed) |
changed |
bool | Whether the resource was modified |
error |
string | Error message (omitted when empty) |
duration_ms |
int | Time spent on this resource in milliseconds |
run_status |
string | Overall run status (converged, degraded, failed) |
source |
string | Always "vigo" |
GET /api/v1/export/cmdb
Permission: fleet.read
Returns a JSON array of CMDB records from the FleetIndex (zero DB queries).
Query parameters:
| Parameter | Default | Description |
|---|---|---|
status |
(all) | Filter by convergence status: converged, degraded, failed, no data, offline |
environment |
(all) | Filter by environment (e.g., prod, staging) |
Response:
[
{
"hostname": "web-01.example.com",
"ip": "10.0.1.10",
"envoy_id": "a1b2c3d4",
"os": "ubuntu",
"os_version": "24.04",
"architecture": "x86_64",
"kernel": "6.8.0-101-generic",
"cpu_count": 4,
"memory_total": 8589934592,
"compliance_status": "compliant",
"last_seen": "2026-03-13T10:05:00Z",
"environment": "prod",
"enrolled_at": "2026-01-15T08:00:00Z",
"uptime": 3628800
}
]
| Field | Type | Description |
|---|---|---|
hostname |
string | Envoy hostname |
ip |
string | IP address |
envoy_id |
string | Envoy UUID |
os |
string | OS family |
os_version |
string | OS version |
architecture |
string | CPU architecture |
kernel |
string | Kernel version |
cpu_count |
int | Number of CPUs |
memory_total |
int | Total memory in bytes |
compliance_status |
string | Current compliance status |
last_seen |
string | RFC 3339 time of last check-in |
environment |
string | Environment tier |
enrolled_at |
string | RFC 3339 enrollment time |
uptime |
int | System uptime in seconds |
Backup
POST /api/v1/backup/snapshot
Permission: backup.manage (rides the gRPC gateway; gate enforced by the gRPC interceptor)
Create an on-demand backup snapshot. Requires admin authentication.
Response (201 Created):
{
"path": "/backup/vigo-20260315T140000Z.tar.gz",
"size": 4218924,
"created_at": "2026-03-15T14:00:00Z"
}
GET /api/v1/backup/download
Permission: backup.manage (rides the gRPC gateway; gate enforced by the gRPC interceptor)
Download the latest backup archive as a .tar.gz file. Requires admin authentication.
Response: Binary stream with Content-Type: application/gzip and Content-Disposition: attachment; filename="vigo-backup.tar.gz".
Peer Replication
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/peer/status |
Returns peer role, primary address, last sync time, files synced, config version. Returns 404 if peer replication not configured. |
| POST | /api/v1/peer/promote |
Promotes the server to primary. Returns 404 if not configured. Split-brain resolved by config version. |
GET /api/v1/export/oscal
Permission: compliance.read
Returns a minimal valid OSCAL Assessment Results JSON envelope (v1.1.2). The findings array is currently empty; future versions will map Vigo compliance data to OSCAL control findings.
Response:
{
"assessment-results": {
"uuid": "...",
"metadata": {
"title": "Vigo Assessment Results",
"last-modified": "2026-03-13T10:00:00Z",
"version": "1.0.0",
"oscal-version": "1.1.2"
},
"results": [
{
"uuid": "...",
"title": "Vigo Compliance Assessment",
"start": "2026-03-13T10:00:00Z",
"findings": [],
"remarks": "Stub: OSCAL findings mapping is not yet implemented."
}
]
}
}
Risk Scoring
Per-envoy and fleet-wide risk scores computed from security scan traits, convergence status, and connectivity data.
GET /api/v1/risk
Permission: fleet.read
Return the fleet-wide risk summary with distribution and top riskiest envoys.
Response (200 OK):
{
"total_envoys": 14,
"scored_envoys": 12,
"avg_score": 23.5,
"max_score": 78,
"distribution": {
"low": 3,
"medium": 4,
"high": 3,
"critical": 2
},
"top_risks": [
{
"envoy_id": "abc123",
"hostname": "db01.prod.example.com",
"score": 78,
"level": "critical",
"factors": [
{"name": "Critical CVEs", "points": 30, "detail": "3 critical CVEs"},
{"name": "Convergence Failed", "points": 15, "detail": "last run had failures"}
]
}
]
}
GET /api/v1/risk/{envoyID}
Permission: fleet.read
Return the risk score breakdown for a single envoy.
Response (200 OK):
{
"envoy_id": "abc123",
"hostname": "db01.prod.example.com",
"score": 78,
"level": "critical",
"factors": [
{"name": "Critical CVEs", "points": 30, "detail": "3 critical CVEs"},
{"name": "High CVEs", "points": 15, "detail": "3 high CVEs"},
{"name": "Hardening Gap", "points": 18, "detail": "Lynis score 82/100"},
{"name": "Convergence Failed", "points": 15, "detail": "last run had failures"}
]
}
Error (404): {"error": "envoy not found"}
GET /api/v1/risk/history
Permission: fleet.read
Daily risk posture snapshots for trend analysis.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
days |
90 |
Number of days of history to return (max 365) |
Response (200 OK):
{
"history": [
{
"date": "2026-03-21",
"avg_score": 23,
"max_score": 78,
"distribution": {
"low": 3,
"medium": 4,
"high": 3,
"critical": 2
},
"scored_envoys": 12,
"total_envoys": 14
},
{
"date": "2026-03-20",
"avg_score": 25,
"max_score": 80,
"distribution": {
"low": 2,
"medium": 5,
"high": 3,
"critical": 2
},
"scored_envoys": 12,
"total_envoys": 14
}
]
}
GET /api/v1/risk/insurance
Permission: fleet.read
Cyber insurance attestation report in JSON format. Includes a per-report identifier, organization name (from branding.org_name), the authenticated user who prepared it, the 90-day attestation period, current fleet risk posture, a daily trend series, and fleet composition.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
download |
— | If 1, sets Content-Disposition: attachment for file download |
Response (200 OK):
{
"report_id": "VIGO-INS-20260417-A3F2B1",
"organization": "Acme Corp",
"prepared_by": "Jane Admin",
"generated_at": "2026-04-17T14:00:00Z",
"generated_by": "Vigo 0.23.25",
"period_start": "2026-01-17",
"period_end": "2026-04-17",
"current_posture": {
"avg_score": 23,
"max_score": 78,
"scored_envoys": 12,
"total_envoys": 14,
"distribution": { "low": 3, "medium": 4, "high": 3, "critical": 2 }
},
"trend": [
{ "date": "2026-04-17", "avg_score": 23, "max_score": 78, "critical": 2, "high": 3, "scored_envoys": 12 }
],
"fleet_summary": {
"total_envoys": 14,
"os_family": { "linux": 13, "windows": 1 }
}
}
The report_id field is a unique identifier per download (format VIGO-INS-<YYYYMMDD>-<6-hex>); include it in any underwriter correspondence to reference a specific report instance.
GET /api/v1/risk/insurance.html
Permission: fleet.read
Formal HTML cyber insurance attestation. Self-contained, suitable for direct viewing, emailing as an attachment, or printing. The document has a cover block with the organization name, a meta-data block (report ID, attestation period, preparer), an executive statement, five numbered sections (current posture, highest-risk assets, 90-day trend, fleet composition, methodology), and an attestation signatory block with four blank signature lines.
Theme behavior: the page follows the viewer's prefers-color-scheme on screen (dark background in dark browser sessions, light background in light sessions), and forces black-on-white when printed or saved as PDF via @media print — so the attachment-delivered version always looks like a formal black-ink document regardless of viewer preference.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
download |
— | If 1, sets Content-Disposition: attachment for file download |
Response: HTML page with Content-Type: text/html.
GET /api/v1/risk/insurance.pdf
Permission: fleet.read
Native PDF version of the cyber insurance attestation, rendered server-side via WeasyPrint. Use this endpoint when emailing the attestation to an underwriter or attaching it to a case file — delivers a real .pdf with consistent cross-client rendering, no "save as PDF" dance.
Response (200 OK): PDF document with Content-Type: application/pdf and Content-Disposition: attachment; filename="cyber-insurance-attestation-YYYY-MM-DD.pdf".
Response (503 Service Unavailable): returned if the server's WeasyPrint runtime is missing (dev environments without the published Docker image). Falls back to the HTML endpoint:
{
"error": "pdf generation unavailable on this server",
"fallback": "/api/v1/risk/insurance.html"
}
The PDF uses the same underlying HTML + print CSS as the .html endpoint, so content is identical — this endpoint simply automates the print-to-PDF step on the server. Render time is typically 500 ms–2 s; subprocess timeout is 30 s.
Risk levels
| Level | Score range |
|---|---|
| Low | 0 -- 19 |
| Medium | 20 -- 39 |
| High | 40 -- 69 |
| Critical | 70 -- 100 |
CVE Impact Search
GET /api/v1/cve/{cveID}
Permission: fleet.read
Search for a specific CVE across the fleet. Returns all affected hosts with package details from security scan traits.
Response (200 OK):
{
"cve_id": "CVE-2024-1234",
"affected_hosts": [
{
"hostname": "web-01.example.com",
"envoy_id": "a1b2c3d4",
"package": "openssl",
"installed": "3.0.2",
"fixed": "3.0.14",
"severity": "high",
"scanner": "trivy"
}
],
"total_hosts": 1
}
Compliance Reports
Framework-specific compliance reports in JSON and printable HTML formats. Each framework has a JSON endpoint and a self-contained, print-friendly HTML endpoint.
| Method | Path | Framework |
|---|---|---|
| GET | /api/v1/report/compliance |
HIPAA |
| GET | /api/v1/report/compliance.html |
HIPAA (HTML) |
| GET | /api/v1/report/hitrust |
HITRUST CSF |
| GET | /api/v1/report/hitrust.html |
HITRUST CSF (HTML) |
| GET | /api/v1/report/soc2 |
SOC 2 Trust Services Criteria |
| GET | /api/v1/report/soc2.html |
SOC 2 (HTML) |
| GET | /api/v1/report/pci |
PCI DSS v4.0 |
| GET | /api/v1/report/pci.html |
PCI DSS v4.0 (HTML) |
| GET | /api/v1/report/cis |
CIS Benchmarks |
| GET | /api/v1/report/cis.html |
CIS Benchmarks (HTML) |
| GET | /api/v1/report/nist |
NIST 800-53 |
| GET | /api/v1/report/nist.html |
NIST 800-53 (HTML) |
| GET | /api/v1/report/iso27001 |
ISO 27001 |
| GET | /api/v1/report/iso27001.html |
ISO 27001 (HTML) |
| GET | /api/v1/report/gdpr |
GDPR |
| GET | /api/v1/report/gdpr.html |
GDPR (HTML) |
| GET | /api/v1/report/nerccip |
NERC CIP |
| GET | /api/v1/report/nerccip.html |
NERC CIP (HTML) |
| GET | /api/v1/report/cyberessentials |
Cyber Essentials+ |
| GET | /api/v1/report/cyberessentials.html | Cyber Essentials+ (HTML) |
| GET | /api/v1/report/iec62443 | IEC 62443 Industrial Cybersecurity |
| GET | /api/v1/report/iec62443.html | IEC 62443 (HTML) |
| GET | /api/v1/report/executive | Executive summary (cross-framework) |
| GET | /api/v1/report/executive.html | Executive summary (HTML) |
JSON response (200 OK):
{
"title": "SOC 2 Trust Services Criteria Report",
"framework": "SOC 2",
"generated_at": "2026-03-23T14:00:00Z",
"findings": [
{
"control_id": "CC6.1",
"title": "Logical and Physical Access Controls",
"status": "satisfied",
"scope": "enforced",
"evidence": "...",
"fleet_percent": 100.0,
"fleet_covered": 5,
"fleet_total": 5,
"doc_count": 0
}
],
"summary": {
"satisfied": 10,
"partial": 2,
"not_satisfied": 1,
"not_applicable": 0
}
}
HTML response: Self-contained page with Content-Type: text/html, suitable for printing or saving as PDF.
Additional Endpoints
These endpoints are documented at summary level; curl -v the running server for full request/response schemas, or consult the handler registrations in server/api/ and server/bootstrap/. If you end up relying on one of these, file a doc-pass to promote it to a full section above.
Config
- GET /api/v1/config/dedup — list configs whose contents deduplicate against other paths (operational hygiene).
Envoys
- GET /api/v1/envoys/{id}/convergence — per-envoy convergence history (status transitions over time).
Inventory
- GET /api/v1/inventory/keys — list all inventory keys known across the fleet (for CMDB joins).
Compliance reports (framework extensions)
Same shape as the existing framework report endpoints: JSON at the bare path, HTML at .html.
- GET /api/v1/report/cisrhel and GET /api/v1/report/cisrhel.html — CIS RHEL benchmark report.
- GET /api/v1/report/cisubuntu and GET /api/v1/report/cisubuntu.html — CIS Ubuntu benchmark report.
- GET /api/v1/report/nydfscyber and GET /api/v1/report/nydfscyber.html — NY DFS Cybersecurity (23 NYCRR 500) report.
Swarm (substrate)
Substrate-level overview of the envoy P2P network. Content-subsystem operations live under their own prefixes; these endpoints describe the transport layer those subsystems ride on.
- GET /api/v1/swarm/status — fleet-wide totals (envoys online, substrate-active, seeders, manifest counts, footprint). (fleet.read)
- GET /api/v1/swarm/peers — per-envoy rollup with
?sort=footprint|blobs|hostnameand?limit=N(0 for unbounded). (fleet.read) - POST /api/v1/swarm/cleanup — dispatch the hidden
vigo swarm _apply-cleanupverb to envoys matching?target=<pat>(default*). Each envoy removes danglingfiles/<label>symlinks and.partial/dirs whose final blob has been assembled. Idempotent. Returns{target, dispatched, task_run_id}— passtask_run_idtovigocli task statusfor per-envoy detail. (admin)
Filecast
Administrator-pushed file distribution on the swarm substrate. These endpoints power vigocli swarm filecast.
- GET /api/v1/swarm/filecast/list — list filecasts (
?revoked=trueincludes revoked tombstones;?json=truereturns full sha256s). (fleet.read) - GET /api/v1/swarm/filecast/match — preview which envoys match a given target expression. (fleet.read)
- GET /api/v1/swarm/filecast/status/{sha256} — per-envoy distribution status for one payload. (fleet.read)
- GET /api/v1/swarm/filecast/broken — payloads with unreachable peers or failed distributions. (fleet.read)
- POST /api/v1/swarm/filecast/distribute — seed a new payload into the swarm for distribution. (admin) Returns 403 when
swarm.enabled: falseorswarm.filecast.enabled: false; denials are recorded asswarm.filecast.deniedin the tamper-evident audit chain. - DELETE /api/v1/swarm/filecast/{sha256} — revoke a payload; agents evict on next check-in. (admin) Allowed when the substrate is disabled (cleanup must work on a halted substrate).
- POST /api/v1/swarm/filecast/purge — garbage-collect revoked filecast tombstones from the manifest; optional
?older_than=<dur>limits the purge to entries past that age (default: all revoked). (admin) Re-poison-safe: agents cannot re-create a purged entry (the agent-gossip path only refreshes existing active seeds).
Puddle
- GET /api/v1/swarm/puddle/fleet — fleet-wide puddle topology + health (ADR-014), aggregated from the per-envoy
puddletrait bypuddlemesh. Returns{generated_at, puddles: [{puddle_pubkey, owner, owner_contested, envoys: [{hostname, user, status, updated_at, initialized, session_unlocked, pair_pending, rekey_pending}], envoy_count, users[], session_unlocked, retired_count, rekey_pending, last_updated, health: {band, reasons[]}}], totals: {puddles, envoys, named, unlocked, unhealthy}}.health.bandishealthy/warning/degraded. (fleet.read) - GET /api/v1/swarm/puddle/names — fleet friendly-name map (ADR-022):
{updated_at, names: {<name>: [{body:{puddle_pubkey, name, claimed_at, supersedes}, signature}, …]}}, aggregated from every envoy'spuddletrait name claims. The server publishes signed claims verbatim and never arbitrates — a name with more than one claimant is returned with all of them (a collision the resolver refuses). Unauthenticated, like/api/v1/fleet/pubkeys— the bindings are public-by-construction; mTLS is the transport guard. Consumed by each agent'spuddle name-map bootstraploop (cached at/var/lib/vigo/swarm/puddle/names.jsonfor thegit-remote-gitbackresolver and thevigo swarm puddle name setcollision check) and byvigocli swarm puddle list.
Gitback
- GET /api/v1/swarm/gitback/fleet — fleet-wide gitback project view (aggregated from per-envoy traits). (fleet.read)
- GET /api/v1/swarm/gitback/projects/{project_id}/roster — canonical member fleet list for one project. Returns
{project_id, handle, founder_puddle_pubkey, members[], dr_scope, member_fleet_count}. The helper daemon's lazy-fetch target at push time. 404 if no envoy in the fleet has reported the project. (fleet.read)
Longdrawer
- GET /api/v1/swarm/longdrawer/fleet — fleet-wide longdrawer mesh view (per-user LAN plaintext sync state aggregated from every envoy's
longdrawertrait). - GET /api/v1/swarm/longdrawer/envoy/{id}/users/{user}/files — per-(envoy, user) file list drilldown for the
/longdrawerpage.
Lockbox
- GET /api/v1/swarm/lockbox/fleet — fleet-wide lockbox mesh view (per-user encrypted sync state aggregated from every envoy's
lockboxtrait). - GET /api/v1/swarm/lockbox/envoy/{id}/users/{user}/files — per-(envoy, user) file list drilldown for the
/lockboxpage.
Scrier
- POST /api/v1/scrier/connect — Phase 1 of a Scrier session: validates the requested envoy + protocol (
ssh/rdp/vnc), creates a session record, and kicks off the agent-side tunnel + SSH/guacd setup in the background. Body{envoy_id, protocol, port, width, height, ssh_public_key?}. Returns{session_id, status}(status iswaitingon accept). (admin) - GET /api/v1/scrier/sessions/{id}/status — poll the setup state of one session. Returns
{status, error?}where status is one ofwaiting/connecting/ready/error. The browser/CLI polls this untilready, then opens the WebSocket relay at/ws/scrier/{id}. (admin; session owner only) - GET /api/v1/scrier/users — list the Scrier OS-user mapping table.
- PUT /api/v1/scrier/users/{username}/osuser — set the mapping for one admin username.
Sandgorgon (bare-metal lifecycle)
Server-side counterparts to vigocli sandgorgon — see the CLI reference for verb-level docs.
- GET /api/v1/sandgorgon/assets — list all managed hardware assets. (fleet.read)
- GET /api/v1/sandgorgon/assets/search — search assets by substring / tag / state. (fleet.read)
- GET /api/v1/sandgorgon/assets/{id} — one asset's full record. (fleet.read)
- GET /api/v1/sandgorgon/boot/{id} — PXE boot payload for one asset. (fleet.read)
- GET /api/v1/sandgorgon/preseed/{id} — preseed file served to installer. (fleet.read)
- GET /api/v1/sandgorgon/dhcp-config — DHCP snippet for PXE routing. (fleet.read)
- GET /api/v1/sandgorgon/events/{assetID} — lifecycle-event stream for one asset. (fleet.read)
- GET /api/v1/sandgorgon/images — list registered OS images. (fleet.read)
- POST /api/v1/sandgorgon/images — register a new OS image. (admin)
- DELETE /api/v1/sandgorgon/images/{id} — deregister an OS image (file not deleted). (admin)
- GET /api/v1/sandgorgon/report/{id} / /api/v1/sandgorgon/report/{id}/html — full lifecycle report. (fleet.read)
- GET /api/v1/sandgorgon/certificates — list all NIST 800-88 sanitization certificates. (fleet.read)
- GET /api/v1/sandgorgon/certificates/{id} / /api/v1/sandgorgon/certificates/{id}/html — one certificate (JSON or printable HTML). (fleet.read)
- POST /api/v1/sandgorgon/commission — register a new asset for lifecycle management. (admin)
- POST /api/v1/sandgorgon/decommission/{id} — decommission: revoke cert, sanitize disks, generate NIST 800-88 certificate. (admin)
- POST /api/v1/sandgorgon/provision/{id} — PXE boot the asset with a registered OS image. (admin)
- POST /api/v1/sandgorgon/import — import assets from CSV / NetBox / Snipe-IT. (admin)
- POST /api/v1/sandgorgon/return-to-available/{id} — return a retired asset to the available pool. (admin)
PXE-serving GETs (boot, preseed, dhcp-config) gate on fleet.read even though a PXE-booting machine itself can't satisfy any web-auth — those endpoints either ride a separate delivery path or hold operator-side state used to write the actual PXE config off-box. The fleet.read gate tightens authenticated-user access without changing PXE flow; revisit when the delivery path is finalized.
Swarm — curator (ADR-024)
The P2P artifact registry. Catalog reads are server-mediated; blob fetches go P2P with the server as a relay fallback. See howto/set-up-swarm.md for operator usage; the design lives in ADR-024.
- GET /api/v1/swarm/curator/list — list every artifact currently published in the fleet (one row per artifact, latest tag set, target scope). (fleet.read)
- GET /api/v1/swarm/curator/inspect/{artifact_id} — full signed catalog entry for one artifact: every platform, every tag, recipient scope, delegation chain. (fleet.read)
- GET /api/v1/swarm/curator/versions/{artifact_id} — version + tag table for one artifact. (fleet.read)
- GET /api/v1/swarm/curator/resolve?artifact_id=…&tag=…&os=…&arch=… — resolve
<name>:<tag>to a concrete blob sha + per-platform metadata. Used bypulland theartifact:executor param. Public-by-construction; artifacts are fleet-readable. (no auth) - GET /api/v1/swarm/curator/blob/{sha256} — server-side relay for a content-addressed blob. Falls back when the agent can't reach a peer that holds it. (no auth — artifacts are fleet-readable; the sha is the capability)
- GET /api/v1/swarm/curator/fleet — fleet-wide aggregator view: who's published what, who's pulled what, blocked-artifact set, retention state. (fleet.read)
- POST /api/v1/swarm/curator/block/{artifact_id} — operator-block: server refuses to resolve or relay this artifact even if it's still in the catalog. Local-on-envoy bytes aren't reached (the block is a control-plane veto). (admin)
- POST /api/v1/swarm/curator/unblock/{artifact_id} — reverse a prior block. (admin)
- POST /api/v1/swarm/curator/s3-credentials — mint a SigV4 credential for the curator S3 object API (ADR-027). Body
{name, scope}; thesecret_access_keyis returned once and never stored (derived). Returns 503 unless the server runs thelocalsecrets backend. (admin) - GET /api/v1/swarm/curator/s3-credentials — list S3 credential metadata (access key id, scope, name, created-at — no secrets). (admin)
- DELETE /api/v1/swarm/curator/s3-credentials/{access_key_id} — revoke an S3 credential. (admin)
Poolq (ADR-029)
The P2P ordered-log queue — Kafka-style retained messages with founder-monotonic seq, founder-only-writable in v1, fleet-readable. All reads are public-by-construction (mTLS is the transport guard); messages are explicitly fleet-readable per ADR-029. See howto/set-up-poolq.md for operator usage; the design lives in ADR-029.
- GET /api/v1/swarm/poolq/topics — every topic the fleet's
poolqmeshaggregator knows about, with per-topic summary (head seq, message count, first/last posted-at) and ablockedflag. Includes a separateblockedarray for topic_ids that are blocked but have no current catalog entry (publisher cleared them after the block landed). (no auth) - GET /api/v1/swarm/poolq/topic/{topic_id}/head — per-topic summary without the messages (cheap "is there new content?" probe). Returns even for blocked topics so consumers see the block status. (no auth)
- GET /api/v1/swarm/poolq/topic/{topic_id}/range?from=N&to=N — message slice in
[from, to]seq range.to=0(or omitted) = "to head". Refuses (404) on a blocked topic. (no auth) - GET /api/v1/swarm/poolq/topic/{topic_id}/log — full topic with every retained message. Convenience for operator views; consumers in steady state should use
/rangewith offsets. Refuses (404) on a blocked topic. (no auth) - POST /api/v1/swarm/poolq/block/{topic_id}?reason=... — operator-block: server refuses to serve the topic's
/rangeand/logfleet-wide regardless of who founded it. Signed messages still exist in posting users' state — only arm -rfon the founder's~/.vigo-poolq/<topic_id>/truly purges. (admin) - POST /api/v1/swarm/poolq/unblock/{topic_id} — reverse a prior block. The 30s
poolqmeshcache is invalidated by both block and unblock so moderation takes effect immediately. (admin)
Database maintenance
Server-internal maintenance verbs. Same role-gate as the rest of the vigocli db … surface.
- POST /api/v1/db/analyze — run SQLite
ANALYZEto refresh the query planner's statistics. (admin) - POST /api/v1/db/prune — prune historical tables (runs, audit, runs-resources) to their configured retention windows. (admin)