feat: documentation site

Docusaurus 3 docs-only site at docsite/, served statically by nginx under
/docs on the marketing host. Covers getting started (self-hosted install
through first server and first licence), the control plane, Vantage HQ,
a reference section and operations.

Wired into docker-compose.site.yml as docsite (3005:80) and into the
image build workflow, rebuilding on its own directory only. Never added
to the self-hosted compose file.
This commit is contained in:
2026-07-28 15:46:33 +01:00
parent d9945882e5
commit f46fb7fc0e
46 changed files with 23697 additions and 2 deletions
+51
View File
@@ -0,0 +1,51 @@
---
id: audit-log
title: Audit log
sidebar_label: Audit log
---
Every mutating API path writes an audit event. The log is at **Audit**.
## What an event carries
| Field | Meaning |
| --- | --- |
| Action | A dotted name, e.g. `server.created`, `settings.updated` |
| Actor | Who did it |
| Target | The object acted on |
| Detail | A short human-readable note |
| Time | When |
## What is recorded
Creation, modification and deletion across the product: servers and enrolments,
keys and assignments, workflow and step changes, runs triggered, monitors and
channels, secret groups and reveals, console sessions opened, settings and
member changes, licence installs.
Reads are not recorded, with one deliberate exception: **revealing a secret**
writes an event, because reading that particular thing is an act rather than a
lookup.
## What is not recorded
- Sign-ins and sign-out.
- Anything inside a console session.
- Step output. That lives in the run log, kept under the workflow retention
setting rather than with the audit log.
## Retention
Audit events are not swept by the workflow log retention setting — that setting
governs run logs only. Audit history stays until the instance does.
:::warning It is a log, not a control
The audit log tells you what happened. It does not restrict what can happen, and
an admin can do anything an admin can do. Use roles for restriction and the log
for accountability.
:::
## Getting events out
`GET /api/audit` returns recent events as JSON and accepts a `limit`. There is
no streaming or push export; if you need events in a SIEM, poll that endpoint.
+70
View File
@@ -0,0 +1,70 @@
---
id: browser-console
title: Browser console
sidebar_label: Browser console
---
An SSH, RDP or VNC session in a browser tab, with no client software and no
inbound port on the target beyond the one the protocol already uses.
Protocol handling is Apache Guacamole's — the control plane proxies a WebSocket
to a **guacd** daemon and manages credentials around it.
## Requirements
- `guacd` running and reachable from the server. The bundled Compose stack
includes it; `GUACD_ADDR` defaults to `guacd:4822`.
- `KEY_ENCRYPTION_KEY` set, since every credential involved is stored encrypted.
- Network reachability **from the control plane to the target** on the protocol
port. This is the one part of Vantage that is not agent-mediated: guacd
connects directly, so a machine reachable only by its agent cannot be
consoled.
## Opening a session
From a server's page, choose **Console**. Then:
1. The UI calls `POST /api/console/connect`, which mints a **one-time** session
token.
2. The browser opens a WebSocket to `GET /api/console/tunnel` with that token.
3. The server marks the token consumed — atomically, so a second use cannot
race — and proxies the connection to guacd.
## Credentials
### SSH
Authenticates with a private key stored in the [key library](./ssh-keys.md). The
key must have its private half uploaded; a public-only key cannot open a
session.
### RDP and VNC
You supply credentials when connecting. They are encrypted, **single-use**, and
consumed when the tunnel opens. They are not retained for the next session.
:::info Why single-use
A stored console credential is a standing grant to that machine for anyone who
can reach the endpoint. Consuming it at tunnel-open means a leaked session token
is worth one connection at most, and only until it is used.
:::
## Session behaviour
Closing the tab ends the session. There is no reconnect and no session
persistence — reopening mints a new token and a new connection.
## Auditing
Opening a console is an audited action, with actor, server and time. What
happens *inside* the session is not recorded: there is no session capture or
keystroke log. If you need that, it has to come from the target machine.
## When it does not work
| Symptom | Cause |
| --- | --- |
| Connects then closes immediately | guacd unreachable — check `GUACD_ADDR` and that the container is up |
| SSH refuses the key | The stored key has no private half, or is not in the target's `authorized_keys` |
| RDP fails on a fresh credential | Credentials are consumed on open; a retry needs them entered again |
| Hangs at connecting | The control plane cannot reach the target on the protocol port |
+78
View File
@@ -0,0 +1,78 @@
---
id: monitors
title: Monitors
sidebar_label: Monitors
---
Monitors check that something is answering. Four types, two places they can run
from, and a notification path when they stop being satisfied.
## Types
| Type | Checks | Options |
| --- | --- | --- |
| `http` | An HTTP(S) URL | method, expected status, keyword that must appear in the body, allow insecure TLS |
| `tcp` | A host and port accept a connection | — |
| `icmp` | A host answers ping | — |
| `tls` | A certificate is valid and not expiring | warn N days before expiry |
An `http` monitor with a keyword is usually the one you want for an application:
a 200 that returns an error page still fails the keyword.
## Where a check runs
Every monitor has a **runner**:
| Runner | Meaning |
| --- | --- |
| `server` | The control plane's scheduler performs the check |
| a server ID | That server's agent performs it locally and reports the result |
Use `server` for anything reachable from the control plane — public endpoints,
your own front door. Use an agent for anything only reachable from inside the
target network: a database on a private subnet, a service bound to localhost, a
device on a management VLAN.
:::tip Agent-run monitors measure what your users can't
A check run from the control plane tells you the service is reachable from
there. A check run on the machine tells you the process is alive. Those are
different questions, and outages usually live in the gap.
:::
## Interval, retries and state
- **Interval** — how often to check.
- **Retries** — how many consecutive failures are tolerated before the state
flips.
A monitor sits in `pending` until its first result. Failures accumulate; once
they exceed `retries`, the monitor goes `down`, an **incident** opens and the
attached notification channels fire. A subsequent success closes the incident.
Retries are what keeps one dropped packet from paging you. Set them to at least
`1` for anything crossing the public internet.
## Notifications
Attach one or more [notification channels](./notification-channels.md) to a
monitor. Channels are shared, so one Slack destination can serve every monitor
you have.
Notification state is tracked per monitor, so a service that is down for six
hours does not send a message per interval.
## Uptime and incidents
The monitor detail page shows:
- **Uptime**, from hourly rollup records — checks performed, how many were up,
and mean latency per hour. Rollups are what make the graph cheap to draw over
long windows.
- **Incidents**, each with a start, a resolution and the cause recorded at the
moment it opened.
## Disabling versus deleting
Disabling stops the checks and keeps the history. Deleting removes the monitor.
Prefer disabling for anything seasonal — the uptime record is usually the part
you wanted.
@@ -0,0 +1,101 @@
---
id: notification-channels
title: Notification channels
sidebar_label: Notification channels
---
A channel is a destination for alerts. [Monitors](./monitors.md) reference
channels by ID, so one destination serves as many monitors as you like.
Manage them at **Settings → Notifications**.
## Types
### Webhook
Posts JSON to a URL you choose.
| Setting | |
| --- | --- |
| `url` | Required |
```json
{
"monitor": "API front door",
"type": "http",
"old_status": "up",
"new_status": "down",
"message": "HTTP 502",
"time": "2026-07-28T09:14:02Z"
}
```
Any response of 300 or above counts as a delivery failure. The request times out
after 10 seconds.
### Discord
| Setting | |
| --- | --- |
| `url` | Discord webhook URL |
Posts the alert as message content.
### Slack
| Setting | |
| --- | --- |
| `url` | Slack incoming webhook URL |
### Telegram
| Setting | |
| --- | --- |
| `token` | Bot token |
| `chat_id` | Target chat |
### SMTP
| Setting | |
| --- | --- |
| `host`, `port` | Required |
| `from`, `to` | Required |
| `username`, `password` | Optional; auth is skipped when the username is empty |
Port `465` uses implicit TLS; anything else uses STARTTLS.
Alert emails are rendered by the same email system that sends licence and
account mail, so a monitor alert and an account email look like the same
product.
## The message
Non-webhook channels all send the same one-line title:
```
[Vantage] API front door (http) is DOWN: HTTP 502
```
Recoveries read `recovered` in place of `is DOWN`. The webhook payload carries
the same information as fields, which is the one to use if you are routing into
something that needs to branch on status.
## Testing
Every channel has a **Test** button. It dispatches a fabricated down event for a
monitor called "Test monitor", through the real delivery path — so a test that
arrives proves the credentials, the network path and the destination, not just
the configuration form.
:::tip Test after every change
Channel settings are only exercised when something breaks, which is the worst
time to discover a stale webhook URL. Re-test after rotating a token.
:::
## Choosing destinations
- Use a **chat channel** for awareness, and make sure someone owns it.
- Use **SMTP** where a durable record matters.
- Use a **webhook** to reach an on-call system that does escalation properly.
Vantage does not do escalation, rotas or acknowledgement; a webhook into
something that does is the intended answer.
+74
View File
@@ -0,0 +1,74 @@
---
id: secrets
title: Secrets vault
sidebar_label: Secrets
---
Key/value pairs, grouped by name, encrypted at rest with AES-256-GCM under
`KEY_ENCRYPTION_KEY`. Two things consume them: workflow steps, and Kubernetes
External Secrets Operator.
## Groups and values
A **group** is a named bundle — `prod-db`, `registry`, `acme-api`. Inside it are
key/value pairs.
Group by consumer, not by type. A group is the unit a workflow step references
and the unit ESO reads, so a group that matches one consumer is one reference;
a group holding everything is over-sharing to every step that needs any of it.
## Managing them
**Secrets → New group**, then add keys.
Values are write-then-hidden. The list shows keys, never values. **Reveal** is a
separate action on a separate endpoint, and it writes an audit event — so
looking at a secret is a recorded act.
Deleting a single key and deleting the whole group are separate operations.
## Using secrets in workflows
Add the group name to a step's `secret_refs`. At execution the group's pairs are
injected into the step's environment:
```bash
# secret_refs: ["registry"]
echo "$REGISTRY_PASSWORD" | docker login registry.example.com -u "$REGISTRY_USER" --password-stdin
```
A workflow can also override `secret_refs` per step, without changing the
library entry.
:::warning A step can print its own secrets
Injection puts values in the environment. If your script echoes them, or runs
with `set -x`, they land in the run log — which is stored on disk and readable
in the UI. Vantage does not scrub step output.
:::
## Kubernetes External Secrets Operator
`GET /api/secrets/:group/values` returns a group's pairs for ESO, authenticated
with a **bearer token** rather than a session.
1. Generate the token at **Settings → Integrations**. It is shown once; only its
SHA-256 is stored.
2. Put it in a Kubernetes secret.
3. Point an ESO `SecretStore` at the endpoint with that bearer token.
The token is rotatable: generating a new one replaces the stored hash and
invalidates the old one immediately.
:::danger This token reads every group
It is instance-wide, not scoped to one group. Treat it as a credential to the
whole vault: store it as a secret in the cluster, never in a manifest in git,
and rotate it when anyone with access leaves.
:::
## What the vault is not
- **Not a password manager.** There is no sharing, expiry or per-user
visibility. Anyone who can sign in and reveal, can reveal.
- **Not versioned.** Overwriting a value loses the previous one.
- **Not recoverable without the key.** If `KEY_ENCRYPTION_KEY` is lost, so is
every value. Back it up separately from the database.
+98
View File
@@ -0,0 +1,98 @@
---
id: servers
title: Servers
sidebar_label: Servers
---
The fleet. Every managed machine runs an agent that connects outbound to the
control plane, and everything else in Vantage — keys, workflows, monitors,
consoles — targets these records.
## Enrolling a server
Covered step by step in [Add your first server](../getting-started/first-server.md).
In short: **Servers → Add server** issues a single-use, one-hour token and shows
a one-liner to run as root on the target machine.
## Lifecycle
| Status | Meaning |
| --- | --- |
| `pending` | Enrolment created; the agent has not registered yet |
| `active` | The agent registered and is syncing |
| `offline` | Last-seen passed the threshold |
The offline sweep runs every two minutes, so a machine that has just gone away
takes a little while to be marked as such. That delay is intentional — a single
missed poll is not an outage.
## The server detail page
### Keys
Which SSH keys are assigned to this machine, and their state. See
[SSH keys](./ssh-keys.md).
### Inventory
Agents report:
| Data | Refreshed |
| --- | --- |
| CPU, memory, swap, load | every 30 seconds |
| Partitions, kernel, full static snapshot | every 15 minutes |
The two carry separate timestamps, so a stale static snapshot beside fresh
metrics is normal rather than a fault.
### OS updates
Agents check for pending package updates hourly and report the count. From the
server page you can:
- **Apply updates** — pushes `ApplyUpdatesCmd` down the command stream. The
agent runs the platform's package manager and reports back.
- **Update agent** — pushes `UpdateAgentCmd` with a target version; the agent
downloads the release, verifies it and replaces itself. See
[Agent updates](../operations/agent-updates.md).
:::warning Applying updates is not scheduled or staged
It runs now, on that machine. If you need ordering, health gates or a canary,
build it as a [workflow](./workflows.md) instead — that is what workflows exist
for.
:::
### Console
Opens a browser SSH, RDP or VNC session. See [Browser console](./browser-console.md).
## Windows servers
Windows agents register, heartbeat, run workflow steps and report inventory.
They do not manage `authorized_keys` — the poll loop stops after the heartbeat
on any non-Linux host. This is a deliberate scope decision, not a gap being
worked on.
## Removing a server
Deleting the server record removes it from the fleet. It does **not** uninstall
the agent, which will keep trying to sync and failing. Uninstall it on the
machine too:
```bash
systemctl disable --now vantage-agent
rm -f /usr/local/bin/vantage-agent /etc/systemd/system/vantage-agent.service
rm -rf /etc/vantage
systemctl daemon-reload
```
Keys previously written to `authorized_keys` stay on disk, because the agent is
no longer running to remove them. Revoke and let the agent apply the change
**before** you delete the server if that matters to you.
## Agent tokens
Each server has its own token. The control plane stores only its SHA-256; the
plaintext exists in the agent's `0600` config and nowhere else. There is no way
to read a token back out of the control plane — if one is lost, re-enrol the
machine.
+113
View File
@@ -0,0 +1,113 @@
---
id: settings
title: Settings
sidebar_label: Settings
---
One page, three groups: **Access**, **Monitoring** and **Integrations**. Plus
the licence, which has its own page.
Settings require the `owner` or `admin` role.
:::info Where instance settings went
Members and single sign-on used to live at `/settings/instance`. They are now
the Access group at the top of this page — splitting "who can sign in" from "how
this instance behaves" produced two half-pages and a nav entry nobody could
distinguish from Settings. The old path still redirects.
:::
## Access
### Members
Add, remove and re-role the people who can sign in.
| Role | Can |
| --- | --- |
| `owner` | Everything |
| `admin` | Everything except owner-only settings |
| `member` | Servers, keys, workflows, monitors, secrets, console |
Local members authenticate with email and a bcrypt-hashed password.
#### Members managed by Vantage HQ
On a cloud instance, people granted access from the HQ portal appear here as
read-only rows with a link to the portal.
:::warning HQ-managed users cannot be edited locally
Changing the role of, or deleting, an `hq`-sourced user is refused with `409`.
HQ owns their role, their password and whether they exist at all — a local
change would be overwritten by the next sync and would leave two writers for one
password hash. Manage them from [People and roles](../hq/people-and-roles.md).
:::
### Single sign-on (OIDC)
Configured per organisation:
| Field | |
| --- | --- |
| Issuer | Your provider's issuer URL |
| Client ID | |
| Client secret | Stored AES-256-GCM encrypted |
Sign-in then goes `/auth/oidc/start` → your provider → `/auth/oidc/callback`.
Local and OIDC users coexist. Keep at least one local owner: if the provider is
misconfigured or unreachable, a local account is the way back in.
## Monitoring
- **Alert defaults** for monitors.
- **Notification channels** — their own page. See
[Notification channels](./notification-channels.md).
## Integrations
### Workflow log retention
How long run logs are kept.
| Value | Meaning |
| --- | --- |
| unset | 30 days |
| a number | that many days |
| `0` | forever |
### ESO read token
The bearer token External Secrets Operator uses to read secret groups. Shown
once, stored as a SHA-256 hash, rotatable. See
[Secrets](./secrets.md#kubernetes-external-secrets-operator).
## Licence
`/settings/license` shows the deployment, tier, server allowance, enabled
features and expiry.
On **self-hosted**, paste a licence here. This works even while the current
licence is expired — that is the way out of degraded mode.
On **cloud**, there is no paste form. The endpoint answers `409 cloud_managed`,
because a cloud licence is written by HQ directly. The page links to the portal
instead.
See [Licensing and entitlements](../hq/licensing-and-entitlements.md).
## Sessions
Sessions are an opaque token in the `km_session` cookie, held in Redis with a
24-hour TTL. There is no per-session management UI; restarting Redis signs
everyone out and affects nothing else.
## Host and organisation guard
On a multi-tenant deployment, a request to `<slug>.vantage.<tld>` resolves the
organisation from the slug and rejects a session belonging to a different one.
The label it looks for comes from `APP_ROOT_LABEL`.
:::warning A wrong `APP_ROOT_LABEL` disables the guard
It does not fail loudly — it simply stops matching, and the host check stops
protecting anything. If you serve the UI on a custom domain, set it to match.
:::
+81
View File
@@ -0,0 +1,81 @@
---
id: ssh-keys
title: SSH keys
sidebar_label: SSH keys
---
Vantage holds a library of public keys and decides, per server, which ones
belong in `/root/.ssh/authorized_keys`. The agent makes the file match.
:::info root only
Vantage manages `/root/.ssh/authorized_keys` and nothing else. There is no
per-user key management. The agent runs as root because writing that file
requires it.
:::
## Adding a key
### Upload one you already have
**Keys → Add key**, paste the public half. Vantage stores the public key and its
fingerprint, and never needs the private half for this path.
### Generate one on a server
Vantage can have an agent generate a keypair on a managed machine
(`GenerateKeyCmd` over the command stream). The public half comes back to the
library. You may optionally upload the private half too, in which case it is
stored **AES-256-GCM encrypted** under `KEY_ENCRYPTION_KEY`.
The JSON representation of a key exposes only `has_private_key` and
`has_passphrase` — never the material. Retrieving a stored private key is its
own endpoint and its own audit event.
:::tip Why store a private key at all
The [browser console](./browser-console.md) needs one to open an SSH session. If
you are not using the console, do not upload private halves.
:::
## Assigning
Assign a key to one or more servers. Within one poll interval — 30 seconds — the
agent picks up the change.
## Revoking
Revocation is **soft**: the assignment gets a `revoked_at` timestamp rather than
being deleted, so the history of who had access to what, and when, survives.
The agent treats a revoked assignment as "not desired" and removes the line from
`authorized_keys` on its next sync.
:::warning Revoking does not close open sessions
It removes the key from the file. An SSH session already established stays up
until it ends. Kill sessions on the machine if that matters.
:::
## What the agent actually does
Each poll:
1. `SyncKeys` returns the desired set of public keys for that server.
2. The agent reads `/root/.ssh/authorized_keys` and computes fingerprints.
3. **If the sets match, it writes nothing.** No disk churn on unchanged state,
which is most polls.
4. If they differ, it writes a temporary file, then `os.Rename()`s it over the
real one and sets mode `0600`.
The rename is atomic, so a machine that dies mid-write keeps the old file
intact. There is no window in which `authorized_keys` is truncated or partial.
:::danger Vantage owns the whole file
The agent rewrites `authorized_keys` to match the desired set. Keys added by
hand on the machine are removed on the next change. If a key must survive, put
it in Vantage.
:::
## Recovering from a lockout
If you have removed every key from a machine and cannot get in, you still have
the console — provided a private key is stored — or out-of-band access from your
hosting provider. Vantage has no backdoor and does not keep a break-glass key.
+142
View File
@@ -0,0 +1,142 @@
---
id: workflows
title: Workflows and steps
sidebar_label: Workflows
---
A **step** is a reusable script with declared inputs, outputs and secret
references. A **workflow** composes steps in order and targets a set of servers.
Running one dispatches the steps to each target's agent and streams the output
back live.
## Steps
A step has:
| Field | Meaning |
| --- | --- |
| `name`, `description` | Library identity |
| `interpreter` | `bash` or `powershell` |
| `script` | The body |
| `declared_inputs` | Named parameters with defaults and descriptions |
| `declared_outputs` | Names this step promises to export |
| `secret_refs` | Vault entries injected as environment variables |
### Passing values between steps
Each step runs with `WORKFLOW_ENV` set to a file path. Anything written there as
`KEY=value` becomes an environment variable for the **later steps of the same
run on the same server**.
```bash
HOSTNAME=$(hostname)
echo "$HOSTNAME"
echo "HOSTNAME=$HOSTNAME" >> $WORKFLOW_ENV
```
That is the whole mechanism. `declared_outputs` documents what a step exports so
the designer can show it; the file is what actually carries the value.
### Secrets
List a vault group in `secret_refs` and its key/value pairs are injected as
environment variables when the step runs. They are not written to the run log
unless your own script echoes them. See [Secrets](./secrets.md).
### The workspace
Every run gets a per-run working directory on each target. Steps share it, so
one step can leave a file for the next. The agent removes it at the end of the
run (`CleanupWorkspaceCmd`).
Do not use it for anything that must outlive the run.
## Default steps
A small library is seeded into every organisation at boot from the image, so a
new install is not staring at an empty page.
:::warning Default steps are read-only
Editing or deleting one is refused with `409`. Seeding rewrites them on every
boot, so an edit would silently revert and a delete would come back at the next
restart — refusing is the honest answer.
To customise one, use the per-step **script override** in the workflow designer,
which belongs to that workflow and is not touched by seeding. To add to the
shared library permanently, a file has to be committed to the repository and the
server image rebuilt.
:::
The UI mirrors this — the step modal opens read-only and Delete is hidden — but
the API is the boundary; the UI is the courtesy.
## Building a workflow
1. **Workflows → New**.
2. Add steps in order from the library.
3. Set inputs per step.
4. Set failure behaviour per step.
5. Choose target servers.
### Failure behaviour
| `on_failure` | Effect |
| --- | --- |
| `stop` | Abort this server's run. Other servers continue |
| `continue` | Record the failure, run the next step anyway |
| `retry` | Re-run the step up to `max_retries`, then treat it as a failure |
### Per-step overrides
A workflow can override a step's script or its secret references without
touching the library entry. This is how you adapt a default step, and it is
scoped to that workflow.
## Running
**Run** snapshots the resolved steps into the run record and dispatches
`RunStepCmd` to each target's agent over the command stream — no waiting for the
next poll.
:::info Runs freeze their steps
The snapshot is why editing a step tomorrow never rewrites what happened today.
A run shows the script that actually executed, not the current library version.
:::
Targets run **in parallel**; steps within one server run **in order**.
## Watching a run
Step stdout and stderr stream back as chunks, are appended to a log file on the
server, and the UI follows them live. Each step records status, attempts, exit
code and its exported environment.
**Cancel** stops a run in progress. Steps already running on an agent finish;
nothing further is dispatched.
## Log retention
Run logs are swept on a schedule set by `workflow_log_retention_days` in
Settings:
| Value | Meaning |
| --- | --- |
| unset | 30 days |
| a number | that many days |
| `0` | keep forever |
## Import and export
Steps export to a JSON file (`vantage.step/v1`) and import back, which is how
you move a step between instances or keep one in version control. There is also
a parse endpoint that turns a pasted script into a draft step by reading its
declared inputs and outputs.
## Practical notes
- A step is a script. It runs as root, on the target, with no sandbox. Review
what you import.
- Keep steps small and single-purpose; compose them in the workflow. That is
what makes the library reusable rather than a folder of near-duplicates.
- PowerShell steps only make sense on Windows targets and bash steps on Linux
ones. Nothing stops you targeting the wrong one; the step simply fails.