Skip to content

Latest commit

 

History

History
512 lines (406 loc) · 22.6 KB

File metadata and controls

512 lines (406 loc) · 22.6 KB

API guide

Last updated: 2026-07-30 · Applies to: OpenWatch v0.8.0 (Eyrie)

Most operators use the web UI for daily work: managing hosts, viewing fleet health, reading compliance state, and triaging alerts. This guide is for automation: scripting repetitive tasks, integrating with CI/CD, or building tooling on top of OpenWatch.

OpenWatch is a single Go binary that serves both the REST API and the embedded React UI over HTTPS on port 8443. All API paths live under /api/v1. The running binary serves its own OpenAPI document as the contract source of truth, and GET /api/v1/version reports the build it came from.

The compliance surface (scan execution + results, remediation, exceptions, posture/drift, audit export, the rule browser) IS exposed over /api/v1. See the compliance API surface (now live). The genuinely-absent pieces (a Prometheus /metrics endpoint, /security-info) are listed under what is genuinely not in the API yet.

When the OpenAPI document and this guide disagree, the OpenAPI document wins.


Conventions

  • Base URL is https://<host>:8443. The server is HTTPS-only. In a default install the certificate at /etc/openwatch/tls/cert.pem is self-signed, so add --cacert /etc/openwatch/tls/cert.pem (or, for a throwaway lab box only, -k) to your curl calls. In production, point --cacert at your own CA bundle instead.
  • Resource identifiers are UUIDs.
  • Timestamps are ISO 8601 / RFC 3339 (for example 2026-06-10T14:30:00Z).
  • Mutating endpoints that exist to be retried safely take a required Idempotency-Key header (a unique string per logical operation). Replaying the same key with the same body returns the original result; replaying it with a different body returns 409.
  • An optional X-Correlation-Id header is propagated through logs and audit events. If you omit it, the server generates one and returns it in the response.
  • Paginated lists (/api/v1/audit/events, /api/v1/scans, /api/v1/alerts, /api/v1/intelligence/events, /api/v1/activity) take limit and an opaque cursor. Each page carries next_cursor; pass its value as the next request's cursor, and stop when it is absent or null. Other lists take limit only, and GET /api/v1/hosts returns the whole fleet.

Authentication

The API accepts two credential types. Both resolve to the same identity and permission set:

  • A Bearer value in the Authorization header. Two kinds exist. An API token (owk_ prefix) is the credential for scripts and CI: it is bound to one role, can carry an expiry, is stored only as a hash, and is revoked at once by DELETE /api/v1/tokens/{id}. It works only while the user who created it may sign in: it is refused while that user is disabled or deleted, and works again if they are re-enabled. A token with no owner is refused. An access token is the short-lived JWT that POST /api/v1/auth/login returns for interactive use and first-time setup; it expires 30 minutes after issue.
  • The browser session cookie (openwatch_session), used by the web UI. Cookie rotation and the on-401 refresh flow are UI concerns and are not covered here. A request that presents the session cookie must also echo the XSRF-TOKEN cookie in an X-CSRF-Token header on every mutation or it gets 403; Bearer requests are exempt from that check.

The contract declares five operations credential-free: GET /api/v1/health, GET /api/v1/version, POST /api/v1/auth/login, POST /api/v1/auth/refresh and POST /api/v1/auth/refresh-cookie. Three more answer an anonymous caller by design and return only what a login page or a client needs before it has an identity: GET /api/v1/capabilities (which capabilities this deployment has, with no license detail), GET /api/v1/license (only tier, status and features until the caller is authenticated), and GET /api/v1/sso/providers/enabled (provider id and name only), together with the SSO redirect pair GET /api/v1/auth/sso/{id}/login and /callback. Two introspection reads are anonymous as well. GET /api/v1/auth/permissions:registry returns the static permission registry (permission ids, descriptions, categories and the built-in role bundles), which this repository publishes in User roles; it never returns users, role assignments, custom roles or deployment settings. GET /api/v1/auth/me/permissions tells a caller what it is: an anonymous caller gets is_anonymous: true and an empty permission list. Everything else requires a valid identity. An anonymous caller gets 401 auth.required; an authenticated caller without the permission gets 403 authz.permission_denied.

GET /api/v1/capabilities reports every capability this deployment has, with whether it is available here, so a client can present a locked control rather than discovering the gate from a 402. It exposes no customer identity or license detail; GET /api/v1/license returns those to an authenticated caller and only tier, status and features to an anonymous one.

Create an API token for automation

Do this once, interactively, with an identity that holds token:write. Pick the narrowest built-in or custom role the job needs and set an expiry; the secret is returned once and never again.

curl --fail-with-body -s --cacert /etc/openwatch/tls/cert.pem \
  -X POST https://localhost:8443/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d @login.json | jq -r '.access_token' > /tmp/ow-access
# login.json holds {"username": "...", "password": "..."} and is deleted after use;
# a password on the command line lands in shell history and in `ps`.

curl --fail-with-body -s --cacert /etc/openwatch/tls/cert.pem \
  -X POST https://localhost:8443/api/v1/tokens \
  -H "Authorization: Bearer $(cat /tmp/ow-access)" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-scanner","role_id":"viewer","expires_at":"2027-01-01T00:00:00Z"}' \
  | jq -r '.token' > ci-token   # store it in your secret manager, not in the repo

--fail-with-body (curl 7.76 and later) makes a 401 or 403 exit non-zero with the error envelope printed, instead of writing the string null into your token file and failing later with a misleading 401. On older curl, check $? and the envelope yourself. expires_at is optional in the contract; set it for every automation credential and rotate it before it lapses. Revoke a token with DELETE /api/v1/tokens/{id} (token:delete); GET /api/v1/tokens lists metadata only.

Log in with a password

The password login is for interactive use and for minting the first API token. The request body is {username, password} with an optional otp (6 digits) when the account has TOTP MFA enrolled. The response is:

{
  "access_token": "…",
  "refresh_token": "…",
  "user": {"id": "…", "username": "admin", "email": "…", "role": "admin"}
}

All later examples assume -H "Authorization: Bearer $TOKEN", where $TOKEN is an API token or an access token.

Refresh, identity, and log out

Method Path Purpose
POST /api/v1/auth/refresh Rotate the refresh token; returns a new access + refresh pair. Body: {refresh_token}.
GET /api/v1/auth/me Return the calling identity (id, username, email, role).
GET /api/v1/auth/me/permissions Return the caller's effective permission strings.
POST /api/v1/auth/logout Revoke the session and refresh token presented as cookies (204). A caller that presents only a Bearer value has nothing this route revokes today: an access token stays valid until it expires (30 minutes), and a refresh token returned in the login body has no revoke route until CP bugs/OW-062 is resolved. Revoke an API token with DELETE /api/v1/tokens/{id}.
POST /api/v1/auth/password:change Change the caller's password. Body: {current_password, new_password}.
POST /api/v1/auth/mfa:enroll Begin TOTP enrollment; returns a provisioning_uri.
POST /api/v1/auth/mfa:verify Confirm an enrolled secret. Body: {otp}.

Authorization

Authorization is permission-based, not role-based, at the endpoint level. Each protected endpoint declares the permission it requires (visible in the served OpenAPI document, for example host:read or host:write). Built-in roles bundle permission sets:

Role Intent
viewer Read-only access
auditor Read plus audit/compliance review
ops_lead Host + scan + remediation operations
security_admin Security configuration
admin Full system administration

A caller missing the required permission receives 403. The full permission and role registry is the source of truth at User roles; the running service exposes it at GET /api/v1/auth/permissions:registry.


Hosts

Method Path Permission Purpose
GET /api/v1/hosts host:read List hosts. Query: environment, tag.
POST /api/v1/hosts host:write Create a host.
GET /api/v1/hosts/{id} host:read Host detail with liveness and compliance summary. Query: framework.
PATCH /api/v1/hosts/{id} host:write Update mutable host fields.
DELETE /api/v1/hosts/{id} host:delete Soft-delete a host (204; sets deleted_at).
GET /api/v1/hosts/{host_id}/monitoring/history host:read Monitoring history.
PUT /api/v1/hosts/{host_id}/maintenance host:write Pause or resume liveness probes for the host (maintenance mode).
POST /api/v1/hosts/{id}/connectivity:check host:connectivity_check Run a connectivity check (idempotent).
GET /api/v1/hosts/{id}/system-info host:read Latest collected system intelligence.
POST /api/v1/hosts/{id}/discovery:run host:write Run host discovery (idempotent).
POST /api/v1/hosts/{host_id}/credentials:resolve credential:read Resolve the effective credential for a host.

Create a host

curl -s --cacert /etc/openwatch/tls/cert.pem \
  -X POST https://localhost:8443/api/v1/hosts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hostname": "rhel9-web01.example.com",
    "ip_address": "10.0.1.50",
    "port": 22,
    "environment": "production",
    "tags": ["web", "rhel9"]
  }'

hostname and ip_address are required; port, display_name, description, environment, tags, group_id, and username are optional. A successful create returns 201; a duplicate hostname in the same environment returns 409.


Credentials

SSH credentials are stored separately from hosts and scoped either to the whole system (scope: system) or to one host (scope: host). Secret material is encrypted at rest and never returned in responses.

Method Path Purpose
GET /api/v1/credentials List credentials (secrets redacted).
POST /api/v1/credentials Create a credential.
GET /api/v1/credentials/{id} Get one credential.
PATCH /api/v1/credentials/{id} Update a credential.
DELETE /api/v1/credentials/{id} Delete a credential.
POST /api/v1/credentials/{id}:clone Clone to a new scope (secret inherited; no plaintext on the wire).

A create body requires scope, name, username, and auth_method (one of ssh_key, password, both). scope_id is the host's UUID and is required when scope is host; it must be absent when scope is system. Either mismatch returns 400 credentials.invalid_scope, and a scope_id that names no active host returns 400 credentials.host_not_found. Provide private_key (and optional private_key_passphrase) and/or password to match the chosen method.


Fleet observability

These endpoints back the dashboard and require read access.

Method Path Purpose
GET /api/v1/fleet/score Aggregate fleet compliance score.
GET /api/v1/fleet/liveness Fleet liveness breakdown.
GET /api/v1/fleet/top-failing-rules Rules failing across the most hosts.
GET /api/v1/fleet/top-failing-hosts Hosts with the most failing rules.
GET /api/v1/fleet/recent-changes Recent compliance state transitions.
GET /api/v1/fleet/connectivity/breakdown Connectivity status counts.

Reading a score field

score_pct is nullable on every surface that carries it, and a client has to handle the null.

Value Meaning
null No rule produced a pass or a fail. Nothing was measured.
0 Rules produced verdicts and all of them failed.

Do not coerce null to 0. They are different answers, and a client that merges them reports an unscanned host as a totally failing one.

The score counts pass and fail only. Skipped, not-applicable and errored rules change neither the numerator nor the denominator. Aggregates are the mean of the scored hosts, each host counting once, with unscored hosts excluded from the mean and still reported in hosts_total, hosts_scored and hosts_without_score.

coverage_status travels beside the score and is one of available, unavailable_unclassified_skips or unavailable_no_outcomes. A coverage percentage is present only when the status is available.

See Scanning and compliance for what the numbers mean, and the OpenAPI contract for the complete envelope including the provenance fields.


Alerts

Method Path Purpose
GET /api/v1/alerts List alerts.
GET /api/v1/alerts/{id} Alert detail.
POST /api/v1/alerts/{id}:acknowledge Acknowledge an alert.
POST /api/v1/alerts/{id}:silence Silence an alert.
POST /api/v1/alerts/{id}:resolve Resolve an alert.
POST /api/v1/alerts/{id}:dismiss Dismiss an alert.

Intelligence and activity

Method Path Purpose
GET /api/v1/intelligence/events Stream of intelligence-collection events.
GET /api/v1/intelligence/state/{host_id} Latest intelligence state for a host.
GET /api/v1/activity Unified recent-activity feed.

System configuration

Connectivity, intelligence-collection, and discovery behavior are configured through the API. These are admin-level controls.

Method Path Purpose
GET / PUT /api/v1/system/connectivity/config Connectivity polling config.
GET /api/v1/system/connectivity/status Connectivity worker status.
GET / PUT /api/v1/system/intelligence/config Intelligence-collection config.
GET / PUT /api/v1/system/discovery/config Discovery config.
POST /api/v1/system/discovery/sweep Trigger a discovery sweep (idempotent).

Users and roles

Method Path Purpose
GET /api/v1/users List users.
POST /api/v1/users Create a user. Body: {username, email, password}.
GET /api/v1/users/{id} Get a user.
PATCH /api/v1/users/{id} Update a user.
DELETE /api/v1/users/{id} Delete a user.
POST /api/v1/users/{id}/roles:assign Assign a role. Body: {role_id}.
POST /api/v1/users/{id}/roles:unassign Remove a role.
GET /api/v1/roles List roles (built-in roles only).
POST /api/v1/roles:create Create a custom role.

For first-admin bootstrap, prefer the CLI (openwatch create-admin) over the API; see Operations.


License

OpenWatch has two tiers: Community, which reports free, and Enterprise, which reports enterprise. Community needs no license file, and a deployment without one reports tier: free with status: no_license. The tiers differ in the scope of an action, not in capability: what one host can do is Community, and the same vocabulary across a fleet is Enterprise. There are no quotas or caps on hosts, scans, users, or retention. An Enterprise-scoped endpoint returns 402 when the active tier lacks the feature.

Method Path Purpose
GET /api/v1/license Current license tier, status, and features.
POST /api/v1/admin/license:verify Dry-run validate a license JWT without installing it.

Audit events

Every meaningful state change writes an audit event. The log is queryable and cursor-paginated, newest first.

Method Path Purpose
GET /api/v1/audit/events List audit events.
GET /api/v1/audit/events/export Download the filtered trail as CSV (default) or JSON (format=json). Requires audit:export.

List query parameters: action, correlation_id, actor_type, resource_type, resource_id, since, until (both RFC 3339), cursor, and limit (1 to 200, default 50). Each page carries next_cursor; pass it as the next request's cursor.

The export takes the same seven filters, returns the whole filtered set newest first, and stops at 10,000 rows; a capped export carries an X-OpenWatch-Export-Truncated header. A query parameter the export does not declare is refused with 400 request.unknown_parameter naming it, so a misspelled filter cannot silently widen an export you will file; the list endpoint ignores unknown parameters as before.


Health and version

These two endpoints are anonymous and are what monitoring should poll.

curl -s --cacert /etc/openwatch/tls/cert.pem https://localhost:8443/api/v1/health | jq
{"status": "healthy", "db_connected": true, "version": "<release version>"}

A healthy response is always status: "healthy", db_connected: true. When the database is unreachable, the endpoint does not return a degraded status body. It returns 503 with the standard ErrorEnvelope (code server.unavailable) instead. GET /api/v1/version returns build metadata (openwatch, kensa, go, commit, build_time).


Error responses

Errors use a single envelope shape, not field-level validation detail:

{
  "error": {
    "code": "hosts.invalid_input",
    "fault": "client",
    "retryable": false,
    "human_message": "ip_address is required",
    "correlation_id": "…"
  }
}

fault is one of client, server, policy, or external. Status codes you will encounter:

Code Meaning
400 Bad request: invalid input or a violated business rule
401 Unauthorized: missing, expired, or invalid credential
402 Payment required: the license tier lacks this feature
403 Forbidden: the caller lacks the required permission
404 Not found
405 Method not allowed
409 Conflict: duplicate resource, or a reused Idempotency-Key with a different body
429 Too many requests: /auth/login or /auth/mfa:verify rate limit exceeded; retry after Retry-After seconds
502 Bad gateway: an external dependency failed
503 Service unavailable: the service is degraded

There is no general per-route API rate limiting in this release. POST /api/v1/auth/login and /api/v1/auth/mfa:verify are the exceptions: they are rate-limited per client IP and return 429 with a Retry-After header over the limit. There is no 422 validation status: validation failures return 400 with the envelope above.

Every error the OpenWatch process generates carries the envelope, including a 404 for an /api/ path that does not exist, a 405, and a 400 for a query parameter that is missing or fails to parse; the message names the parameter and never repeats the rejected value. A proxy or load balancer in front of OpenWatch produces its own 502, 503 or 504 bodies, so a client should treat any non-2xx whose body is not JSON as an infrastructure error rather than fail on the parse.


Operations: the CLI and systemd

Automation that manages the deployment itself (rather than calling the API) uses the openwatch binary and systemd, not Docker. The subcommands are listed once, in the environment reference.

Day-to-day lifecycle:

systemctl status openwatch
systemctl restart openwatch
journalctl -u openwatch -f

Configuration lives in /etc/openwatch/openwatch.toml, with a fixed set of environment overrides named OPENWATCH_<SECTION>_<KEY> (the loader recognizes only the variables listed in the environment reference and ignores any other) and the database DSN in /etc/openwatch/secrets.env (OPENWATCH_DATABASE_DSN). For full install and configuration steps, see docs/guides/INSTALLATION.md.


Compliance API surface (now live)

As of v0.2.0, the compliance workflow IS exposed over api/v1 (it is no longer worker-internal only):

  • Scans: trigger with POST /api/v1/hosts/{id}/scans; browse durable per-scan history + per-rule evidence + OSCAL export under /api/v1/scans and /api/v1/scans/{id} (scan:read).
  • Remediation: request/approve/reject + execute/rollback under /api/v1/remediation/requests (sub-actions :approve, :dry-run, :execute, :reject, :rollback).
  • Compliance exceptions: request via /api/v1/hosts/{id}/exceptions, browse the fleet queue via /api/v1/compliance/exceptions, then mutate with /api/v1/exceptions/{xid}:approve, :reject, or :revoke.
  • Posture + drift: per-host /api/v1/hosts/{id}/compliance and /api/v1/hosts/{id}/compliance/trend; fleet /api/v1/fleet/score.
  • Audit export: GET /api/v1/audit/events (audit:read; filterable via query parameters, cursor-paginated) and GET /api/v1/audit/events/export (audit:export: auditor, security_admin, admin), a synchronous CSV/JSON download of the same filtered set (capped at 10,000 rows, newest-first).
  • Rule browser: /api/v1/rules (the Kensa rule-library read model).

What is genuinely not in the API yet

  • A Prometheus /metrics endpoint and a /security-info endpoint: both are roadmap items (use GET /api/v1/health for liveness today). Do not script against them until they appear in the served OpenAPI document.

Kensa is the SSH-based compliance scanning engine OpenWatch invokes to run scans; see Scanning and compliance for how it integrates.


What's next