Compare commits

..
13 Commits
Author SHA1 Message Date
mrhid6 53629450e0 docs(monitors): heartbeat monitors and metric alerts guides; mask ping tokens in bundled nginx log
Deploy / deploy (push) Successful in 4m3s
2026-09-17 09:46:49 +00:00
mrhid6 f230b66384 docs(mfa): add multi-factor authentication guide
Deploy / deploy (push) Successful in 2m39s
2026-09-16 14:18:41 +00:00
mrhid6 a1960b26ea docs(patching): no dispatch in the last 15 minutes, running patches finish, 45 minute reboot wait
Deploy / deploy (push) Successful in 1m58s
2026-09-15 13:49:19 +00:00
mrhid6 d5e5377fae docs: patching - maintenance windows, patch policies and runs 2026-09-15 13:25:45 +00:00
mrhid6 4623d9b7b0 feat: Updated docs
Deploy / deploy (push) Successful in 5m59s
2026-09-15 10:46:09 +00:00
mrhid6 58a5230405 feat: Updated docs
Deploy / deploy (push) Successful in 1m51s
2026-09-15 10:18:23 +00:00
mrhid6 50b5ee2964 docs: removed old env vars
Deploy / deploy (push) Successful in 1m22s
2026-09-10 11:20:05 +00:00
mrhid6 7762629634 chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md
Deploy / deploy (push) Successful in 2m47s
2026-09-10 09:18:57 +00:00
mrhid6 e8455c99fe docs: Updated install doc to include nginx instructions
Deploy / deploy (push) Successful in 57s
2026-09-10 08:43:43 +00:00
mrhid6 fbfb4b8608 fix: Fixed docker compose repo url
Deploy / deploy (push) Successful in 2m30s
2026-09-09 11:23:08 +00:00
mrhid6 fcedb12f39 docs: fix mcp runner redaction mechanism, note create_workflow's scope and fan-out checks
Deploy / deploy (push) Successful in 1m42s
2026-09-09 08:23:44 +00:00
mrhid6 a0c20299a5 docs: document agent access over MCP and token tag restrictions 2026-09-09 08:20:42 +00:00
mrhid6 c4bc4b68bf docs: describe the redesigned api keys page
Deploy / deploy (push) Successful in 1m35s
2026-09-08 14:12:53 +00:00
33 changed files with 1125 additions and 180 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ on:
branches:
- main
# DOCS_URL, DOCS_BASE_URL and APP_URL are repo variables, and editing one
# pushes no commit so nothing would rebuild on its own. This is the
# pushes no commit - so nothing would rebuild on its own. This is the
# escape hatch for that, and DOCS_BASE_URL in particular is baked into
# every asset path in the build.
workflow_dispatch:
@@ -27,7 +27,7 @@ jobs:
docker login ${{ vars.DOCKER_HOST }} \
-u "${{ secrets.REGISTRY_USER }}" --password-stdin
# One image, one directory the whole repository is the docs site,
# One image, one directory - the whole repository is the docs site,
# so there is nothing to detect a change in. It rebuilds on every
# push, which is what a docs site wants anyway.
#
+13 -9
View File
@@ -6,18 +6,18 @@ tracking `main`, search indexed at build time by
`@easyops-cn/docusaurus-search-local` so nothing external is keyed or called.
Extracted from the `vantage` monorepo with its history, where it was
`docsite/`. The whole repository is the site there is no subdirectory.
`docsite/`. The whole repository is the site - there is no subdirectory.
It documents the **product**, not the codebase: each repository's own CLAUDE.md
remains the contributor's map, and the two are allowed to differ in altitude but
not in fact. Five sections Getting started, Vantage, Vantage HQ, Reference,
Operations with `sidebars.ts` authored by hand so ordering is a decision
not in fact. Five sections - Getting started, Vantage, Vantage HQ, Reference,
Operations - with `sidebars.ts` authored by hand so ordering is a decision
rather than a filename accident.
## A path, not a subdomain
`*.vantage.hostxtra.co.uk` is the per-tenant instance namespace, and the control
plane's `APP_ROOT_LABEL` resolves an instance from the label before `vantage`
plane's `APP_ROOT_LABEL` resolves an instance from the label before `vantage` -
so a `docs.` label would be read as a tenant slug. Hence `/docs` on the
marketing host instead.
@@ -25,7 +25,7 @@ marketing host instead.
asset 404s:**
1. `DOCS_BASE_URL` (default `/docs/` in the Dockerfile)
2. the proxy location that routes here which must also sort **above** the
2. the proxy location that routes here - which must also sort **above** the
catch-all forwarding to `site:3003`, or Next answers the 404 first
3. the directory the runtime stage serves from,
`/usr/share/nginx/html/docs`
@@ -39,7 +39,7 @@ Unlike the three Next apps this builds to static files, so its runtime stage is
## Design tokens
`src/css/custom.css` holds `vantage-site`'s `web/app/globals.css` token blocks
**copied verbatim** same names, same values. This is the one place the tokens
**copied verbatim** - same names, same values. This is the one place the tokens
are not consumed through Tailwind: everything below the token block maps
Docusaurus's `--ifm-*` variables onto them. Docusaurus already stamps
`data-theme` on `<html>`, which is the selector the dark block keys on, so the
@@ -47,7 +47,7 @@ built-in toggle needed no wiring.
The rule holds all the same: **no rule in that file outside the token blocks may
carry a hex.** The one concession is `static/img/favicon.svg`, which must, for
the same reason the email layout must a browser tab cannot read a token.
the same reason the email layout must - a browser tab cannot read a token.
**Nothing enforces the match across the four front ends, and they are now in
four repositories.** A token change here follows one in `vantage-site`; there is
@@ -71,13 +71,13 @@ Not part of a self-hosted install.
## CI/CD
`.gitea/workflows/deploy.yml`, on every push to `main` plus `workflow_dispatch`.
One image, `vantage/vantage-docs:latest`, from the repository root the whole
One image, `vantage/vantage-docs:latest`, from the repository root - the whole
repository is the site, so there is nothing to detect a change in and it
rebuilds on every push.
**No build args are passed, deliberately.** `DOCS_URL`, `DOCS_BASE_URL`,
`APP_URL` and `HQ_URL` all have correct defaults in the Dockerfile, and passing
an unset repo variable would blank one rather than leave it alone which for
an unset repo variable would blank one rather than leave it alone - which for
`DOCS_BASE_URL` means every asset path in the build. Change them in the
Dockerfile. The monorepo's workflow passed none either; this preserves that.
@@ -88,3 +88,7 @@ Dockerfile. The monorepo's workflow passed none either; this preserves that.
| `DOCKER_HOST` | Variable | registry host used for image tags |
Despite the name the workflow **does not deploy**; it only builds and pushes.
## Writing style
Never use em dashes (the long dash character) anywhere: code, comments, UI copy, docs, commit messages. Use a plain hyphen ` - `, a comma, a colon, or split the sentence instead.
+1 -1
View File
@@ -27,7 +27,7 @@ RUN npm run build
#
# Docusaurus emits a fully static site, so unlike web/ and site/
# there is no Node server at runtime. alpine-slim is roughly a quarter the size
# of caddy:alpine, and nothing here needs automatic TLS the host proxy
# of caddy:alpine, and nothing here needs automatic TLS - the host proxy
# terminates it.
FROM nginx:alpine-slim AS runner
+3 -3
View File
@@ -1,6 +1,6 @@
# The user documentation, served at vantage.hostxtra.co.uk/docs.
#
# A fragment, composed on top of the vantage repository's own file see the
# A fragment, composed on top of the vantage repository's own file - see the
# vantage-site fragment for the full command.
#
# A path on the marketing host, not a subdomain: *.vantage.hostxtra.co.uk is
@@ -8,8 +8,8 @@
# would read a `docs.` label as a tenant slug.
#
# The proxy location that routes here must sort ABOVE the catch-all forwarding
# to site:3003, or Next answers the 404. It forwards the FULL path it does
# not strip /docs so the proxy location, DOCS_BASE_URL and the directory the
# to site:3003, or Next answers the 404. It forwards the FULL path - it does
# not strip /docs - so the proxy location, DOCS_BASE_URL and the directory the
# image serves from (/usr/share/nginx/html/docs) must all agree. When they do
# not, the HTML loads and every asset 404s.
#
+2 -3
View File
@@ -33,8 +33,7 @@ shown at the top, and it is the value Vantage HQ asks for.
1. Sign in at [Vantage HQ](https://vantage-hq.hostxtra.co.uk). If you have no
account yet, see [Accounts and signup](../hq/accounts-and-signup.md).
2. On **Overview**, choose **License my own install**. Once you already have an
instance, the same page offers **Buy a plan** instead.
2. On **Overview**, choose **Create a self-hosted instance**.
3. Choose **Self-hosted**, then the **Free** plan.
4. Paste your instance ID, give the instance a name you will recognise, and
click **Create licence**.
@@ -48,7 +47,7 @@ From there, use **Download licence** or **Copy to clipboard**.
## 4. Install the licence
Back in your install, open the **Licence** page, paste the licence and save.
Back in your self-host install, open the **Licence** page, paste the licence and save.
Your instance confirms the licence was issued to it, then shows your tier,
server allowance and expiry date.
+2 -2
View File
@@ -58,7 +58,7 @@ as a Windows service and starts it.
Windows agents register, heartbeat, report inventory, run workflow steps,
serve the browser console, check and apply OS updates, and report workloads
(services and containers). Managing `authorized_keys` is a Linux-only
feature, and so is package inventory and CVE scanning the vulnerability
feature, and so is package inventory and CVE scanning - the vulnerability
feeds this project uses carry no Windows data.
:::
@@ -89,7 +89,7 @@ Open the server's page. Within a minute or two you should see:
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| Script stops at "Unsupported architecture" | The machine is not 64-bit x86 or ARM |
| "Checksum mismatch!" | An interrupted download, or a proxy rewriting the response. Run it again |
| "Could not determine latest agent version" | The machine cannot reach the release host, or `GITEA_HOST` is not set on your control plane |
| "Could not determine latest agent version" | The machine cannot reach the release host, `gitea.hostxtra.co.uk` |
| Service runs, server stays `pending` | The machine cannot reach the agent port. Test it from that machine, not from the control plane |
| Registers, then goes `offline` | A firewall or proxy allows the first connection but drops the long-lived one |
| "Server limit reached" | Your licence allowance is full. Raise it in Vantage HQ, or remove a server you no longer manage |
+177 -22
View File
@@ -13,8 +13,10 @@ You need:
- A Linux host with **Docker** and the **Compose plugin**.
- A DNS name pointing at that host. People use it for the web UI, and your
agents use it too.
- A reverse proxy in front of Vantage that terminates TLS. It needs to handle
both the web UI and the agent port, `9090`, which speaks HTTP/2.
- A way to terminate TLS. The stack ships an nginx reverse proxy that routes
the web UI and API on plain HTTP; you can terminate TLS in that nginx (see
[step 4](#4-the-reverse-proxy)) or in a proxy you already run in front of it.
The agent port, `9090`, speaks HTTP/2.
- Those two ports reachable: the web port from wherever your people are, and
`9090` from every machine you intend to manage.
- Outbound access from the control plane, and from each managed machine, to
@@ -23,13 +25,17 @@ You need:
The stack brings MongoDB, Redis and the console daemon with it, so there is no
database to provide.
## 1. Get the Compose file
## 1. Get the Compose file and proxy config
```bash
mkdir -p /opt/vantage && cd /opt/vantage
curl -fsSLO https://gitea.hostxtra.co.uk/mrhid6/vantage/raw/branch/main/deploy/docker/docker-compose.yml
mkdir -p /opt/vantage/nginx && cd /opt/vantage
curl -fsSLO https://gitea.hostxtra.co.uk/vantage/vantage-app/raw/branch/main/deploy/docker/docker-compose.yml
curl -fsSL -o nginx/vantage.conf https://gitea.hostxtra.co.uk/vantage/vantage-app/raw/branch/main/deploy/docker/nginx/vantage.conf
```
The Compose file mounts `./nginx/vantage.conf` into the `nginx` service, so it
must sit at that path next to `docker-compose.yml`.
## 2. Write the environment file
Create `/opt/vantage/.env`:
@@ -42,8 +48,8 @@ GRPC_HOST=vantage.example.com:9090
# 32 bytes as 64 hex characters. Generate it with the command below.
KEY_ENCRYPTION_KEY=
# The host serving agent downloads.
GITEA_HOST=gitea.hostxtra.co.uk
# Optional: host port for the bundled nginx proxy. Defaults to 80.
# NGINX_HTTP_PORT=80
```
Generate the encryption key:
@@ -52,16 +58,6 @@ Generate the encryption key:
openssl rand -hex 32
```
Then make sure the `server` service passes `GITEA_HOST` through, by adding this
line to its `environment:` block in `docker-compose.yml`:
```yaml
GITEA_HOST: ${GITEA_HOST}
```
Without it, the install command you hand to a new server cannot work out which
agent to download.
:::danger Keep the encryption key safe
`KEY_ENCRYPTION_KEY` encrypts SSH private keys, vault secrets, single sign-on
client secrets and console credentials. If you lose it, all of those become
@@ -82,7 +78,7 @@ docker compose up -d
docker compose ps
```
Five services start: `mongo`, `redis`, `guacd`, `server` and `web`.
Six services start: `mongo`, `redis`, `guacd`, `server`, `web` and `nginx`.
Check the server got through startup:
@@ -94,9 +90,9 @@ On first boot it prepares the database and loads the built-in workflow step
library. If it stops during that, it will say why, and it is meant to stop
rather than run in a half-prepared state.
## 4. Put a proxy in front
## 4. The reverse proxy
Terminate TLS at your reverse proxy and route **one hostname to two backends**:
The `nginx` service routes **one hostname to two backends**:
| Path | Backend |
| -------------------------------------------------------------------------------- | ------------- |
@@ -105,7 +101,138 @@ Terminate TLS at your reverse proxy and route **one hostname to two backends**:
Both rules are required. The web app forwards nothing to the API, so a proxy
that sends the whole hostname to `web:3000` serves the interface and answers
`404` to every request it makes starting with the login form.
`404` to every request it makes - starting with the login form. If you replace
the bundled nginx with your own proxy, it must route the same way.
This is the shipped `nginx/vantage.conf`:
```nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Heartbeat ping URLs carry a credential. Log them with the token replaced;
# the header form (X-Vantage-Token) is never logged by this format.
map $request_uri $vantage_log_uri {
"~^/public/hb/(?!start(?:[/?]|$)|fail(?:[/?]|$))[^/?]+(?<hb_rest>.*)$" "/public/hb/***$hb_rest";
default $request_uri;
}
log_format vantage '$remote_addr - $remote_user [$time_local] '
'"$request_method $vantage_log_uri $server_protocol" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
upstream vantage_server {
server server:8080;
keepalive 16;
}
upstream vantage_web {
server web:3000;
keepalive 16;
}
server {
listen 80;
listen [::]:80;
server_name _;
access_log /var/log/nginx/access.log vantage;
client_max_body_size 10m;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# REST API. The browser console is a WebSocket and workflow logs stream,
# so no buffering and a long read timeout.
location ^~ /api/ {
proxy_pass http://vantage_server;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}
location ^~ /auth/ {
proxy_pass http://vantage_server;
}
location ^~ /public/ {
proxy_pass http://vantage_server;
}
# /install, /install.ps1, /update, /update.ps1
location ~ ^/(install|update)(\.ps1)?$ {
proxy_pass http://vantage_server;
}
location / {
proxy_pass http://vantage_web;
}
}
```
The `Upgrade`/`Connection` headers and `proxy_buffering off` are not optional:
without them the browser console cannot open its WebSocket and live workflow
logs arrive in bursts or not at all.
The `map` and `log_format` at the top are optional but recommended. A
[heartbeat monitor](../vantage/heartbeat-monitors.md) ping URL contains a secret
token, and this format writes it to the access log as `***`. If you use your
own proxy instead, mask `/public/hb/<token>` the same way, or have jobs send
the token in the `X-Vantage-Token` header.
### Terminating TLS in nginx
The shipped config speaks plain HTTP, which is right when another proxy or load
balancer in front of it already terminates TLS. To terminate TLS in the bundled
nginx instead, mount your certificate and add a `443` server, redirecting `80`
to it.
In `docker-compose.yml`, extend the `nginx` service:
```yaml
nginx:
ports:
- 80:80
- 443:443
volumes:
- ./nginx/vantage.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs:/etc/nginx/certs:ro
```
In `nginx/vantage.conf`, replace the `listen 80` lines of the existing server
with the TLS ones, and add a redirect server:
```nginx
server {
listen 80;
listen [::]:80;
server_name vantage.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name vantage.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
# ... the rest of the shipped server block, unchanged ...
}
```
### Agent traffic on 9090
Agents connect to port `9090`. Vantage does not terminate TLS itself, so put
that port behind your proxy too, with a certificate valid for the name in
@@ -113,6 +240,33 @@ that port behind your proxy too, with a certificate valid for the name in
by default, and the symptom is agents that register once and then stop
responding.
With the bundled nginx, remove `9090:9090` from the `server` service's `ports`,
add `9090:9090` to the `nginx` service, and add this server to
`nginx/vantage.conf`:
```nginx
server {
listen 9090 ssl;
listen [::]:9090 ssl;
http2 on;
server_name vantage.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
# Command streams are long-lived; do not time them out.
grpc_read_timeout 1h;
grpc_send_timeout 1h;
location / {
grpc_pass grpc://server:9090;
}
}
```
`grpc://` is plain HTTP/2 (h2c) to the container, which is what the server
speaks.
For a private network where TLS is not required, you can instead set
`tls: false` in each [agent's config](../reference/agent-config.md) and let
agents reach the port directly.
@@ -128,8 +282,9 @@ Continue with [First login](./first-login.md).
| Check | Expected |
| ---------------------------------------------- | ------------------------------- |
| `docker compose ps` | five services `running` |
| `docker compose ps` | six services `running` |
| `curl -s localhost:8080/auth/bootstrap-status` | JSON saying bootstrap is needed |
| `curl -s localhost/auth/bootstrap-status` | the same JSON, through nginx |
| `nc -z your-host 9090` | open |
| `docker compose logs server` | no fatal errors |
+1 -1
View File
@@ -27,7 +27,7 @@ The instance is created and licensed as soon as payment confirms.
### Self-hosted
Install your control plane first a licence binds to its instance ID.
Install your control plane first - a licence binds to its instance ID.
1. Set **Deployment** to **Self-hosted**.
2. Choose a **Plan** and configure its features and server allowance.
+3 -13
View File
@@ -40,20 +40,10 @@ Four features are enabled per instance rather than bundled into a tier:
No tier includes them by default; you enable them on the instances that need
them.
## Entitlements: desired and granted
[Patching](../vantage/patching.md) is available on every tier and is not a
feature you enable: security patching is never paid for.
Each instance has one entitlement row holding two configurations:
| | Meaning |
| ----------- | ---------------------------- |
| **Desired** | What you last asked for |
| **Granted** | What a payment has confirmed |
Checkout is built from **desired**. A licence is only ever signed from
**granted**. An abandoned checkout therefore leaves a desired that reached
nothing and changed nothing.
### Increases and reductions
## Increases and reductions
An increase takes effect when payment confirms, and the entitlement is promoted
desired → granted.
+7 -33
View File
@@ -35,10 +35,11 @@ Their invitation link stays valid until they use it to set that password.
### Removing someone
Removing them from the account removes their portal access. See below for what
happens to their instance access.
Removing them from the account removes their portal access.
## Instance access
It also removes their instance access immediately, and ends any session they have open.
## Cloud instance access
Granting access to a **cloud** instance creates a real account inside that
instance, marked as managed by Vantage HQ.
@@ -53,39 +54,12 @@ They then sign in at the instance itself, and that keeps working whether or not
Vantage HQ is reachable. Revoking removes the account outright, so access ends
immediately.
### Granting
### Granting access and roles
On an instance record, **Members → Add**, choose an account member and an
On an instance record, **Members → Add**, choose the HQ account member and an
instance role (`owner`, `admin`, `member`).
One person gets one account per instance, so granting twice is refused rather
than quietly creating a second.
### Roles inside an instance
Independent of the account role. Someone can be an account `member` and an
instance `owner`, or the reverse.
### Revoking
Removes their access immediately, and ends any session they have open.
:::warning Self-hosted instances cannot be granted from HQ
Vantage HQ cannot add or remove people in a self-hosted install. Manage them in
the install itself, at **Settings → Access**.
:::
## Passwords
One Vantage HQ password covers you and every cloud instance you have been given
access to. Change it in the portal and it changes everywhere, within about 15
minutes at worst if an instance is briefly unreachable.
Those people cannot change that password inside an instance, so there is only
ever one place it is set.
:::warning HQ-managed users are read-only in the instance
Changing the role of, or removing, someone managed by Vantage HQ has to be done
from the portal. Inside the instance those rows are read-only, with a link back
here.
:::
These roles are independant of HQ account roles.
+2 -2
View File
@@ -15,7 +15,7 @@ Install first, then link and claim. Step by step in
## Paid
**Install first.** A licence is issued to one instance, so your control plane
has to exist and report an instance ID before you can buy for it the same
has to exist and report an instance ID before you can buy for it - the same
precondition Free has.
```mermaid
@@ -36,7 +36,7 @@ flowchart LR
## Upgrading an instance you already have
Paste the same instance ID you already hold an install on Free moves to the
Paste the same instance ID you already hold - an install on Free moves to the
paid plan in place, keeping its ID and its history. An ID belonging to another
account is refused.
+26 -26
View File
@@ -7,16 +7,16 @@ sidebar_label: Backup and restore
`vantagectl` is a separate command-line tool that backs up and restores the
MongoDB database behind a Vantage control plane. It talks to MongoDB directly,
never to the Vantage API, so it works against a control plane that is down,
half-migrated, or gone exactly the situation a backup tool has to survive.
half-migrated, or gone - exactly the situation a backup tool has to survive.
For the store-level overview what holds what, and why the database alone is
not a backup see [Backups](./backups.md). This page covers the tool.
For the store-level overview - what holds what, and why the database alone is
not a backup - see [Backups](./backups.md). This page covers the tool.
:::danger The key comes first
Vantage encrypts SSH private keys, key passphrases, vault secrets, SSO client
secrets and console credentials with `KEY_ENCRYPTION_KEY`. **It is not in your
backup, and it is not recoverable.** A database restored without it is
permanently unreadable not degraded, not partially readable, unreadable.
permanently unreadable - not degraded, not partially readable, unreadable.
Store it wherever you store the credentials you could not rebuild: a password
manager, a secrets vault outside this control plane, a piece of paper in a
@@ -26,7 +26,7 @@ safe. Anywhere but next to the archive.
## What a backup holds
Every collection in the database, the index definitions each one needs to be
useful again, and a SHA-256 **fingerprint** of `KEY_ENCRYPTION_KEY` never the
useful again, and a SHA-256 **fingerprint** of `KEY_ENCRYPTION_KEY` - never the
key itself. The fingerprint is what lets a later `restore` or `verify` tell you
that the key you are holding is the wrong one, before it writes a database
nobody can read.
@@ -37,8 +37,8 @@ nobody can read.
true whenever Redis itself restarts.
- **The vulnerability database.** It is re-pulled automatically on next boot.
- **Agent state on managed servers.** Nothing needs re-enrolling: agents
reconnect on their own, because `servers.agent_token_hash` the thing an
agent authenticates with is itself in the backup.
reconnect on their own, because `servers.agent_token_hash` - the thing an
agent authenticates with - is itself in the backup.
:::note Pin the version
The image is published on each `vantagectl/v*` release and tagged with that
@@ -80,7 +80,7 @@ backup:
```
`backup.enabled` defaults to `false`, and the chart refuses to render if it is
turned on without both `backup.image` and `backup.pvcName` a backup needs a
turned on without both `backup.image` and `backup.pvcName` - a backup needs a
known image and somewhere durable to land, and guessing at either is worse than
refusing to start. `backup.exclude` names collections to leave out (recorded in
the archive's manifest, so an archive never claims to be complete when it is
@@ -88,13 +88,13 @@ not), and `backup.successfulJobsHistoryLimit` / `backup.failedJobsHistoryLimit`
/ `backup.resources` behave exactly as they do on any other `CronJob`.
`backup` refuses to run without `KEY_ENCRYPTION_KEY` set in the environment,
unless you pass `--allow-no-key` for a deployment that genuinely stores no
unless you pass `--allow-no-key` - for a deployment that genuinely stores no
encrypted data. Everywhere else, treat the refusal as the tool doing its job.
## Where to put the archive
`--out -` streams the tarball to stdout instead of writing a file, and every
line of progress output goes to stderr so piping the archive into something
line of progress output goes to stderr - so piping the archive into something
else is always safe, nothing progress-related lands in the stream.
Into `restic`:
@@ -109,7 +109,7 @@ Into S3:
vantagectl backup --out - | aws s3 cp - s3://my-backups/vantage-$(date +%F).tar.gz
```
An archive is as sensitive as a raw database dump it carries every SSH key
An archive is as sensitive as a raw database dump - it carries every SSH key
assignment, every secret group, every session-adjacent setting, in a form the
right `KEY_ENCRYPTION_KEY` can decrypt. Whatever you pipe it into should
encrypt it at rest; `vantagectl` itself does not.
@@ -123,23 +123,23 @@ vantagectl verify /backups/vantage-backup-vantage-20260907T020000Z.tar.gz \
Each line of output answers a different question:
- **`Archive`** every member's checksum still matches; the tarball has not
- **`Archive`** - every member's checksum still matches; the tarball has not
been truncated or corrupted.
- **`Archive key`** / **`Your key`** the fingerprint stored in the archive
- **`Archive key`** / **`Your key`** - the fingerprint stored in the archive
next to the fingerprint of the `KEY_ENCRYPTION_KEY` in your environment.
- **`Key match`** whether those two fingerprints agree.
- **`Live probe`** given `--mongo-uri`, `verify` goes one step further and
- **`Key match`** - whether those two fingerprints agree.
- **`Live probe`** - given `--mongo-uri`, `verify` goes one step further and
decrypts a real ciphertext value from that database with the key you hold.
A fingerprint match proves two archives agree about a key; only the probe
proves the key in your hand actually reads the data.
`verify` exits non-zero the moment anything above is wrong, which is what makes
it worth putting on a schedule a backup job that "succeeded" last night is
it worth putting on a schedule - a backup job that "succeeded" last night is
not the same claim as a backup that will actually restore.
## Looking inside an archive
`inspect` prints an archive's manifest and touches no database at all no
`inspect` prints an archive's manifest and touches no database at all - no
`--mongo-uri`, no key. It is what to run against an archive of unknown origin,
before deciding whether it is the one you want:
@@ -173,8 +173,8 @@ collection named in the archive before loading it. `--force` always needs a
second assurance, in one of two forms:
- `--confirm-db NAME`, naming the target exactly. A mismatch is refused. This
works everywhere on a terminal and in a Kubernetes Job, a CI step or a cron
entry alike and is the form to script.
works everywhere - on a terminal and in a Kubernetes Job, a CI step or a cron
entry alike - and is the form to script.
- Nothing, on a terminal: `--force` alone prompts you to type the target
database's name back, a deliberate pause before something destructive.
@@ -190,7 +190,7 @@ log lines are still there, joined to freshly restored runs. Dropping them
instead would delete data you never asked to delete.
`restore` also refuses when the archive's key fingerprint does not match the
`KEY_ENCRYPTION_KEY` in your environment see "When the key is wrong" below.
`KEY_ENCRYPTION_KEY` in your environment - see "When the key is wrong" below.
## The restore drill
@@ -210,7 +210,7 @@ monthly:
```
3. Drop the scratch database. It served its purpose.
The failure this catches is not "the archive is corrupt" `verify` alone
The failure this catches is not "the archive is corrupt" - `verify` alone
catches that. It is "the archive is fine but nobody can actually stand a
control plane back up from it," which only a real restore proves.
@@ -221,13 +221,13 @@ If `restore` finds the archive's key fingerprint does not match the
`--ignore-key-mismatch` proceeds anyway, but says plainly which collections
will come back with ciphertext nobody can read:
- `keys` SSH private keys and passphrases
- `secrets` the vault
- `auth_providers` OIDC/SSO client secrets
- `console_sessions` RDP/VNC credentials
- `keys` - SSH private keys and passphrases
- `secrets` - the vault
- `auth_providers` - OIDC/SSO client secrets
- `console_sessions` - RDP/VNC credentials
There is no way to recover that ciphertext afterwards. If you have reached
this point, the right key was lost along with the chance to read those rows
this point, the right key was lost along with the chance to read those rows -
the fix is to re-enter each of them by hand (re-upload SSH keys, re-save vault
secrets, reconfigure SSO), not to keep searching for a way to decrypt what is
already in the database.
+2 -2
View File
@@ -27,7 +27,7 @@ Store the key somewhere other than the server it protects.
:::info Use `vantagectl`
[**Backup and restore**](./backup-and-restore.md) is the supported way to take
and restore a backup. It writes an archive that carries a fingerprint of
`KEY_ENCRYPTION_KEY` never the key so a restore taken with the wrong key
`KEY_ENCRYPTION_KEY` - never the key - so a restore taken with the wrong key
**refuses** rather than silently producing a database whose secrets nobody can
read. It also checksums every archive member before writing anything, and
refuses to restore into a database that already holds data. A plain
@@ -52,7 +52,7 @@ docker compose exec -T mongo mongodump --archive --gzip --db vantage \
:::warning
This archive records nothing about which `KEY_ENCRYPTION_KEY` it was taken
under. Restoring it with the wrong key produces a database that looks intact
and is not every secret in it is silently unreadable until something tries
and is not - every secret in it is silently unreadable until something tries
to decrypt one.
:::
+95 -13
View File
@@ -5,15 +5,34 @@ sidebar_label: API tokens
---
A session cookie is fine for a browser. A script, a CI job or a cron task
needs something it can hold onto instead an API token.
needs something it can hold onto instead - an API token.
## Creating one
**API Keys**, in the Access group of the sidebar. The page is reachable at
every role: any member may create and revoke their own keys, and owner and
admin additionally see every key in the instance. Give it a name, a role
(owner, admin or member) and one or more scopes, and optionally an expiry. The value is shown
once, in full, immediately after creation:
admin additionally see every key in the instance.
**Create key** opens a dialog with four decisions, in the order they matter:
1. **Name and role.** The name says what will use the key - the CI pipeline,
the script, the cluster. The role list offers your own role and everything
below it, never above.
2. **Scopes.** A grid of resources against read and write. Ticking write also
ticks read, since write already satisfies read on the same resource.
**Read-only everywhere** and **Clear all** set the whole grid at once.
3. **Restrict to servers tagged.** Optional; see [Tag
restrictions](#tag-restrictions) below.
4. **Expiry.** Each option names the date it resolves to, so "90 days" and
"7 December 2026" are the same choice read two ways.
Beneath them sits a preview line that reads the key back as a sentence -
*"gitea-ci-deploy acts as admin, may read and write servers and workflows,
read secrets, and stops working on 7 December 2026."* Read it before you
submit; an over-granted key is far easier to spot in a sentence than in a grid
of ticks.
The value is shown once, in full, immediately after creation:
```
vt_8f2c1a9e4b6d0735a1c8e29f4b0d6e17...
@@ -21,13 +40,17 @@ vt_8f2c1a9e4b6d0735a1c8e29f4b0d6e17...
That is the only time you will see it. Vantage stores a hash of the token,
never the value itself, so if you lose it there is no support ticket that gets
it back create a new token and revoke the old one.
it back - create a new token and revoke the old one.
The panel showing it also carries a ready-made `curl` line and a summary of
what was granted, so the key can go straight into a secret store without a
second trip to the list.
## Scopes
A token can reach only what its scopes name. There are eight resources, each
with a `:read` and a `:write` scope, and holding `:write` on a resource also
satisfies a `:read` requirement for it you do not need to tick both.
A token can reach only what its scopes name. Each resource has
a `:read` and a `:write` scope, and holding `:write` on a resource also
satisfies a `:read` requirement for it - you do not need to tick both.
| Resource | Covers |
| ----------- | --------------------------------------------------- |
@@ -38,16 +61,75 @@ satisfies a `:read` requirement for it — you do not need to tick both.
| `monitors` | Monitors, incidents, uptime and notification channels |
| `vulns` | Vulnerability findings, packages and scan rules |
| `workloads` | Containers and systemd units, including control actions and logs |
| `patching` | Maintenance windows, patch policies and patch runs. `patching:write` creates and edits them and starts or cancels runs |
| `settings` | Instance settings, members, single sign-on, licence, and token management itself |
A token created with only `servers:read` can list and inspect servers but
cannot run a workflow against them, touch a key, or read a secret each of
cannot run a workflow against them, touch a key, or read a secret - each of
those needs its own scope.
## Tag restrictions
A key can be pinned to part of the fleet as well as part of the API. **Restrict
to servers tagged** in the create dialog offers the tag keys and values already
in use across your servers, and the key then reaches only servers carrying
**every** pair listed - the restriction is an AND, not an OR. Leaving it empty
is the opposite: no restriction at all, the whole fleet.
The restriction is fixed at creation, like the role and the scopes. Changing
what a credential already deployed in CI can reach, with no record of what it
could reach before, is worse than requiring a rotation - so to widen or narrow
one, create a replacement and revoke the old key.
Restricted keys show their tags as chips beside their scopes in the list.
Unrestricted keys show nothing there, which is the common case.
The restriction applies to every API token, not only ones handed to an
[MCP agent](../vantage/mcp.md). A CI token, a monitoring script's token and
an agent's token are all held to the same tag scope check wherever the
service layer resolves servers - the mechanism does not know or care what
kind of caller is holding the token.
Nor can a token created under a restricted token reach further than its
creator: minting a new key from an already-restricted key can only narrow
the tag set, never widen or drop it. A `env=staging` token cannot mint a
token that also sees production.
## Reading the key list
Each key is one record rather than a row of bare strings:
- **Key** - the name, the `vt_` prefix hint, the holder (when viewing all
keys) and the role.
- **Scopes** - one chip per resource, its access half tinted: `rw` in accent,
`r` in grey. A key with nothing granted says so in words rather than showing
a dash.
- **Lifetime** - a bar showing how much of the key's issued life is left,
with the date beside it. Four states, and the label always says which:
| Bar | Means |
| --- | ----- |
| Green | More than seven days left |
| Amber | Seven days or fewer - rotate it |
| Red | Already expired; the key no longer authenticates |
| Grey, full width | No expiry at all |
A key issued before the instance's maximum lifetime was tightened also
carries *"outside the current policy - rotate when convenient"*. That is a
prompt, not a failure: the cap is never applied retroactively and the key
keeps working.
- **Last call** - when the key last authenticated, or *Never used*.
Above the list, four counts summarise the same thing at fleet scale: keys
listed, keys expiring within seven days, keys that never expire, and keys
never used since they were issued. They describe the list as filtered, so they
change with the **My keys** / **All keys** toggle.
## A token never outranks its owner
A token's role can be at most the role of the person who created it, and its
effective role is **recomputed on every request** as the lower of the two
effective role is **recomputed on every request** as the lower of the two -
not fixed at creation. Demote the person from owner to member and every token
they hold drops to member from that request onward. Remove the person and
every token they hold stops working immediately: a token has no existence
@@ -73,7 +155,7 @@ curl -H "Authorization: Bearer vt_…" https://acme.vantage.example.com/api/serv
```
Everything else about the [REST API](./rest-api.md) applies the same way it
does to a session JSON errors, audit logging, licence gating on writes
does to a session - JSON errors, audit logging, licence gating on writes -
except that authority comes from the token's role and scopes rather than a
signed-in person's role.
@@ -97,12 +179,12 @@ already been deleted.
This page covers the token model. Every route, request and response shape is
in the generated OpenAPI reference, served by **your own instance** at
`/api/docs` not this documentation site, since the routes and their shapes
`/api/docs` - not this documentation site, since the routes and their shapes
are specific to your install. The raw document is at `/api/openapi.json`.
:::danger Not the External Secrets token
The bearer token read by `GET /api/secrets/:group/values` for the Kubernetes
External Secrets Operator is a **separate credential** a single instance-wide
External Secrets Operator is a **separate credential** - a single instance-wide
value, rotated from Settings, that reaches only that one endpoint. It is not an
API token and an API token cannot be used in its place: the two are checked by
different code, and neither substitutes for the other. See
+1 -2
View File
@@ -17,7 +17,6 @@ it is absent.
| `REDIS_USERNAME` | no | | Redis 6+ ACL user. Leave empty against a legacy `requirepass` instance, which authenticates with the password alone |
| `REDIS_PASSWORD` | no | | Leave empty for an unauthenticated Redis. Both of these exist so an install can use a managed Redis rather than the bundled one |
| `KEY_ENCRYPTION_KEY` | yes in practice | | 64 hex characters (32 bytes) for AES-256-GCM. Required for private keys, vault secrets, OIDC client secrets and console credentials |
| `GITEA_HOST` | yes in practice | `gitea.example.com` | Host serving agent releases; used to build the install scripts and download URLs. The default is a placeholder that will not resolve, so set it to `gitea.hostxtra.co.uk` |
| `GUACD_ADDR` | no | `guacd:4822` | The [browser console](../vantage/browser-console.md) daemon |
| `PROXY_ADVERTISE_HOST` | no | `server` | The hostname **guacd** uses to reach the control plane's console relay. Wrong here and every console session fails at connect with guacd unable to resolve the relay |
| `PROXY_LISTEN_HOST` | no | `0.0.0.0` | Interface the ephemeral relay listeners bind. Narrow it only if guacd shares a known interface |
@@ -25,7 +24,7 @@ it is absent.
| `VANTAGE_LICENSE` | no | | A licence supplied at startup, so an automated install does not have to paste one in |
| `VANTAGE_TRIVY_DB_REF` | no | `ghcr.io/aquasecurity/trivy-db:2` | Where the vulnerability database is pulled from. Point it at a mirror for an air-gapped install |
| `VANTAGE_VULNDB_DISABLED` | no | | `true` switches [vulnerability scanning](../vantage/vulnerabilities.md) off entirely. Findings already stored are still served, and still shown as stale |
| `TRUSTED_PROXIES` | no | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | Comma-separated CIDRs or addresses of proxies allowed to set `X-Forwarded-For`. The shipped Docker Compose and Helm chart default to the private RFC1918 ranges, which covers Nginx Proxy Manager on the Docker bridge network and Traefik on a Kubernetes pod CIDR. An operator whose proxy sits on a public address must set this themselves, or every visitor behind it shares one address for rate-limiting purposes. Unset entirely (outside those shipped defaults) trusts none, so the client address is the direct peer. **On a LAN-only install, narrow this to your proxy's address.** The RFC1918 default trusts every private range, so a client on 192.168.0.0/16 reaching the server directly is itself a "trusted proxy" and can put whatever it likes in `X-Forwarded-For` and, on the public status route, in `X-Forwarded-Host`. Behind a proxy on a public address, or with no proxy at all, that is not reachable; on a flat LAN it is |
| `TRUSTED_PROXIES` | no | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | Comma-separated CIDRs or addresses of proxies allowed to set `X-Forwarded-For`. The shipped Docker Compose and Helm chart default to the private RFC1918 ranges, which covers Nginx Proxy Manager on the Docker bridge network and Traefik on a Kubernetes pod CIDR. An operator whose proxy sits on a public address must set this themselves, or every visitor behind it shares one address for rate-limiting purposes. Unset entirely (outside those shipped defaults) trusts none, so the client address is the direct peer. **On a LAN-only install, narrow this to your proxy's address.** The RFC1918 default trusts every private range, so a client on 192.168.0.0/16 reaching the server directly is itself a "trusted proxy" and can put whatever it likes in `X-Forwarded-For` - and, on the public status route, in `X-Forwarded-Host`. Behind a proxy on a public address, or with no proxy at all, that is not reachable; on a flat LAN it is |
:::danger `KEY_ENCRYPTION_KEY` has no recovery path
It encrypts SSH private keys, vault secrets, OIDC client secrets and console
+2 -2
View File
@@ -9,7 +9,7 @@ sidebar_label: Ports and networking
| Port | Service | Who connects | Expose publicly |
| ------- | ----------- | -------------------------------- | --------------- |
| `3000` | web | Browsers, via your reverse proxy | Yes, behind TLS |
| `8080` | server API | Your reverse proxy | Not directly proxied |
| `8080` | server API | Your reverse proxy | Not directly - proxied |
| `9090` | server gRPC | Agents | **Yes** |
| `4822` | guacd | The server | No, firewall it |
| `27017` | MongoDB | The server | No |
@@ -85,7 +85,7 @@ On a private network you can skip TLS instead, by setting `tls: false` in each
| everything else | `web:3000` |
The web app forwards nothing to the API. Sending the whole hostname to
`web:3000` loads the interface and every request it makes answers `404`
`web:3000` loads the interface and every request it makes answers `404` -
including the login form.
- Both backends must be the **same** hostname and certificate. The browser
+6 -6
View File
@@ -22,7 +22,7 @@ or is not 64 hex characters.
**Every request 404s and the interface loads fine.** Your reverse proxy sends
the whole hostname to `web:3000`. `/api`, `/auth`, `/public`, `/install*` and
`/update*` belong to `server:8080` and the web app forwards nothing see
`/update*` belong to `server:8080` and the web app forwards nothing - see
[Ports and networking](./ports-and-networking.md#reverse-proxy-notes). The
tell is `curl -si https://<your-host>/auth/bootstrap-status` returning HTML
with `x-powered-by: Next.js` instead of JSON.
@@ -124,7 +124,7 @@ User-Agent: Vantage-Monitor/1.0 (+https://vantage.hostxtra.co.uk)
That string is the hook to allow the check through. In whichever product is
doing the blocking, add a rule that skips bot protection, managed rules and rate
limiting for requests carrying it Cloudflare, AWS WAF, Azure Front Door,
limiting for requests carrying it - Cloudflare, AWS WAF, Azure Front Door,
Akamai, Fastly, Imperva, Sucuri, ModSecurity, nginx and HAProxy all match on a
request header. The shape of the rule is the same everywhere:
@@ -137,7 +137,7 @@ Three details are worth getting right:
exact match breaks silently on an upgrade, and the symptom is a monitor that
goes down on deploy day.
- **Keep the rule narrow.** Scope it to the specific host and path being
monitored. A User-Agent is not a secret anyone can send it so a rule that
monitored. A User-Agent is not a secret - anyone can send it - so a rule that
skips protection site-wide on that string alone is a bypass you have
published.
- **Allow the source address too, where you can.** Combining the User-Agent with
@@ -170,7 +170,7 @@ needs `host`, `port`, `from` and `to`, and Telegram needs both `token` and
## A status page 404s or shows no data
**404, and it should be published.** Check the **Published** toggle on the
page's editor an unpublished page answers *not found* for everyone,
page's editor - an unpublished page answers *not found* for everyone,
including you, with no session exemption. Also check the host: the public URL
is `<your-instance>.vantage.<yourdomain>/status/<page-id>`, the same
per-instance subdomain everything else in Vantage uses. A wrong or missing
@@ -178,11 +178,11 @@ subdomain resolves to no instance at all, which is also a 404.
Third possibility: `/public` is not routed to the server. Check with
`curl -si https://<your-instance>.vantage.<yourdomain>/public/status/<page-id>`
JSON is correct, HTML carrying `x-powered-by: Next.js` means the proxy sent
- JSON is correct, HTML carrying `x-powered-by: Next.js` means the proxy sent
that prefix to the web app.
**Loads, but shows an explanation instead of components.** This is not a
fault it is the page working as designed. It means either the licence has
fault - it is the page working as designed. It means either the licence has
lapsed (a self-hosted instance past its grace period, or a cloud instance
between billing events) or the current tier does not include the **Status
pages** feature. Fix the licence or the plan and the same link starts serving
+128
View File
@@ -0,0 +1,128 @@
---
id: heartbeat-monitors
title: Heartbeat monitors
sidebar_label: Heartbeat monitors
---
Backups and cron jobs fail silently. Nothing goes down, the job just doesn't
run. A heartbeat monitor turns that silence into an incident: your job calls a
URL each time it finishes, and Vantage alerts when the call stops arriving.
It is the opposite of every other [monitor](./monitors.md). Vantage does not
check anything; it waits to be told.
## Creating one
1. Go to **Monitors** and choose **New monitor**.
2. Pick **Heartbeat**.
3. Set **Expected every (minutes)**: how often the job runs.
4. Set **Grace (minutes)**: how late a ping may be before it counts as missed. The
default is 5 minutes.
5. Attach [notification channels](./notification-channels.md) and save.
The next page shows the **ping URL**, with ready-made `curl` commands.
:::warning Copy the URL now
The URL contains a secret token, and it is shown only this once. Vantage
stores a hash of the token, not the token itself, so it cannot show it to you
again. If you lose it, use **Rotate token** on the monitor page to get a new
one.
:::
A new heartbeat stays **pending** until its first ping. It will not raise an
incident before the job has ever run, so you can create the monitor before you
deploy the job.
## Sending pings
Add a call to the end of your job:
```bash
# success
curl -fsS -m 10 --retry 3 https://vantage.example.com/public/hb/<token>
```
Two more calls are optional:
| Call | Meaning |
| ------------------------------ | ------------------------------------------------------------------ |
| `/public/hb/<token>` | The job succeeded. The monitor goes up. |
| `/public/hb/<token>/start` | The job started. Vantage measures the time until the next success. |
| `/public/hb/<token>/fail` | The job failed. An incident opens straight away. |
Each accepts `GET` or `POST`, and answers `OK`.
A typical cron job using all three:
```bash
URL=https://vantage.example.com/public/hb/<token>
curl -fsS -m 10 "$URL/start"
if backup-job 2>/tmp/backup.err; then
curl -fsS -m 10 --retry 3 "$URL"
else
tail -c 1024 /tmp/backup.err | curl -fsS -m 10 --data-binary @- "$URL/fail"
fi
```
The body of a `/fail` request becomes the incident's cause and appears in the
alert, so sending the end of the job's error output tells whoever gets paged
what went wrong. Only the first 1 KB is kept.
### Keeping the token out of URLs
URLs end up in logs: your own proxy's, a load balancer's, an ingress
controller's. If that matters, send the token in a header instead and call the
path without it:
```bash
curl -fsS -m 10 -X POST -H "X-Vantage-Token: <token>" https://vantage.example.com/public/hb
curl -fsS -m 10 -X POST -H "X-Vantage-Token: <token>" https://vantage.example.com/public/hb/start
curl -fsS -m 10 -X POST -H "X-Vantage-Token: <token>" https://vantage.example.com/public/hb/fail
```
If a request carries a token in both places, the one in the URL is used.
Vantage's own request log, and the access log of the nginx bundled with the
[self-hosted install](../getting-started/self-hosted-install.md#4-the-reverse-proxy),
replace the token in a ping URL with `***`. Nginx's error log and any proxy you
run in front of Vantage (on Kubernetes, the ingress controller) are not masked,
so the header is the safer choice there.
## When an incident opens
| Situation | Incident cause |
| ---------------------------------------------------------- | --------------------------------- |
| No ping within the expected period plus grace | `no ping since <time>` |
| A `/start` was not followed by a success within the grace | `started <time>, never finished` |
| The job called `/fail` | `reported failure: <body>` |
Overdue heartbeats are checked every 30 seconds, so an alert can arrive up to
half a minute after the deadline. Only a success ping closes the incident.
The grace time does two jobs: it is how late a regular ping may be, and how
long a run may take after `/start`. Set it longer than your slowest normal run.
## Duration
When a job calls `/start` and then succeeds, the time between the two is
recorded and shown as **Duration** on the monitor page, in place of the
response time other monitors show. A backup that used to take 4 minutes and now
takes 40 is worth knowing about before it starts overrunning.
## Limits and behaviour worth knowing
- **One request per second per token and call type.** A `/start` and the
success ping in the same second are both accepted; two success pings in the
same second are not, and the second gets `429`.
- **Unknown token, disabled monitor:** both answer `404`, with no hint which.
- **Disabling and re-enabling** a heartbeat resets it to pending, so a monitor
switched off for a month does not page the moment it is switched back on.
- **Rotating the token** stops the old URL working immediately, so update the
job straight after.
- A heartbeat is never run by an agent and has no interval or retries.
## API tokens and agents
Creating a heartbeat through the API or the [MCP agent](./mcp.md) returns the
token once in the create response, as `heartbeat_token`. A monitor created
through MCP is saved disabled, like any other.
+236
View File
@@ -0,0 +1,236 @@
---
id: mcp
title: Agent access (MCP)
sidebar_label: Agent access (MCP)
---
The Model Context Protocol is how an LLM agent - Claude, or any other MCP
client - talks to a tool server over a defined wire format instead of a
one-off integration. Connecting Vantage to an agent this way lets it answer
questions about your fleet and, if you choose to let it, act on it: run a
workflow, apply updates, or draft a step for you to review.
:::info Requires the MCP feature on your licence
Agent access is a per-instance feature you enable on a paid plan. Without it,
`/api/mcp` answers `feature_unavailable` and the token form refuses to mint a
token carrying an `mcp:*` scope. See
[Licensing and entitlements](../hq/licensing-and-entitlements.md).
:::
## Minting a token
An agent authenticates the same way any script does: with an
[API token](../reference/api-tokens.md). Two scopes govern what it can reach
on the MCP endpoint specifically, on top of whatever resource scopes
(`servers:read`, `workflows:write`, and so on) it also needs:
- **`mcp:read`** - permission to reach the MCP endpoint at all and use its
read-only tools.
- **`mcp:write`** - permission to use its write tools: running a workflow,
applying updates, assigning a key, or creating a step, workflow or monitor.
Without `mcp:write`, write tools are not merely refused when called - they
are left out of the tool listing entirely, so a read-only agent never learns
they exist. A tool it cannot see is not one it can talk a user into asking
for.
Each tool additionally needs the ordinary resource scope for what it touches
- `run_workflow` needs `workflows:write`, `list_servers` needs
`servers:read`, and so on, exactly as the [scopes table](../reference/api-tokens.md#scopes)
describes for any other token. `mcp:read` or `mcp:write` decides whether the
MCP surface is open to this token at all; the resource scope decides what it
can do once there.
## Restricting a token by tag
An agent token benefits from the same [tag
restriction](../reference/api-tokens.md#tag-restrictions) every API token
can carry, and it is the main lever for keeping an agent's blast radius
small. A token restricted to `env=staging` sees only staging hosts from
`list_servers`, gets a not-found error asking about a production host by ID,
and cannot run a workflow that would touch one.
**Worked example: a staging-only agent token.** Create a token with
`mcp:read`, `mcp:write`, `servers:read`, `workflows:read` and
`workflows:write`, restrict it to `env=staging`, and give it to your agent.
It can list and inspect staging servers and run a workflow already
configured to target them. Point it at a workflow whose saved targets
include even one production host, though, and `run_workflow` refuses the
call outright - see [what an agent cannot do](#what-an-agent-cannot-do)
below for why that refusal is all-or-nothing rather than "run on the
servers it can see."
## Connecting a client
Agent access lives at `/api/mcp` on your instance, over the streamable HTTP
transport, and takes a bearer token the same way the REST API does. It has
no separate authentication of its own - a browser tab with a signed-in
session does not count, only a real API token does.
Point an MCP client at it with a configuration block like this:
```json
{
"mcpServers": {
"vantage": {
"type": "http",
"url": "https://YOUR-INSTANCE/api/mcp",
"headers": { "Authorization": "Bearer vt_your_token_here" }
}
}
}
```
The endpoint is stateless and answers POST requests only: it holds no
per-connection session, so each call is self-contained and there is nothing
to resume. A GET request - a client probing for the endpoint, or a browser
navigating to it directly - gets a `405`, by design, rather than a page. That
is the protocol-correct response from a server that does not offer the
GET/SSE half of MCP, not a misconfiguration.
## The tool list
Every tool below calls the same service code the REST API and the Vantage
UI call. Nothing in this list has its own path to the database - the MCP
surface is a presentation layer over existing authority, not a new one.
| Tool | What it does | Scope required |
| --- | --- | --- |
| `list_servers` | List servers in the fleet, optionally filtered by tags. | `mcp:read`, `servers:read` |
| `get_server` | Get one server's OS, online state and tags. | `mcp:read`, `servers:read` |
| `list_monitors` | List monitors and their current state. | `mcp:read`, `monitors:read` |
| `get_monitor_status` | Get one monitor's up/down/pending state, last check and last error. | `mcp:read`, `monitors:read` |
| `list_incidents` | List monitor incidents (outages), most recent first. A tag-restricted token only sees metric alert incidents for servers inside its tags. | `mcp:read`, `monitors:read` |
| `get_monitor_samples` | Get one monitor's recent raw check results. | `mcp:read`, `monitors:read` |
| `list_workflows` | List workflows with step count, target count and whether each is scheduled. | `mcp:read`, `workflows:read` |
| `get_workflow` | Get one workflow's ordered steps, targets and schedule. | `mcp:read`, `workflows:read` |
| `get_run` | Get one workflow run's status and per-server counts. | `mcp:read`, `workflows:read` |
| `get_run_logs` | Get the ordered log lines for one server within one run. | `mcp:read`, `workflows:read` |
| `list_pending_updates` | List outstanding package updates, fleet-wide or for one server. | `mcp:read`, `servers:read` |
| `list_vulnerabilities` | List known CVEs affecting the fleet, one row per CVE/package pair. | `mcp:read`, `vulns:read` |
| `get_server_packages` | List installed packages on one server, optionally filtered by name. | `mcp:read`, `vulns:read` |
| `search_fleet` | Search every server's installed packages by name across the fleet. | `mcp:read`, `vulns:read` |
| `list_audit_events` | List recent audit log events. | `mcp:read`, `settings:read` |
| `list_secret_names` | List secret group and key names - metadata only, never a value. | `mcp:read`, `secrets:read` |
| `run_workflow` | Run a workflow against the servers it is already saved to target. | `mcp:write`, `workflows:write` |
| `cancel_run` | Cancel an in-progress workflow run. | `mcp:write`, `workflows:write` |
| `apply_updates` | Apply pending OS package updates on real servers. | `mcp:write`, `servers:write` |
| `update_agent` | Trigger the Vantage agent on real servers to update itself. | `mcp:write`, `servers:write` |
| `assign_key` | Assign an SSH key to real servers. | `mcp:write`, `keys:write` |
| `create_step` | Create a reusable workflow step. | `mcp:write`, `workflows:write` |
| `create_workflow` | Create a workflow from existing step IDs. | `mcp:write`, `workflows:write` |
| `create_monitor` | Create a monitor, saved disabled. Supports every type, including `heartbeat` (the ping token is returned once) and `metric`. | `mcp:write`, `monitors:write` |
A known limitation worth calling out on `search_fleet`: its `version_below`
argument is not implemented. Passing it gets you an error explaining that
correct version ordering depends on each host's own package manager
(dpkg, rpm, apk each order versions differently), which the tool has no way
to resolve, so it refuses rather than guess with a naive string comparison.
Omit `version_below` and it returns every matching install instead, for you
or the agent to filter.
## Running a workflow: targets are fixed, not chosen
`run_workflow` does not take a target list. A workflow runs the servers and
tags it was already saved with - an agent cannot redirect it at a different
part of the fleet on the fly. Before starting the run, Vantage checks that
those saved targets sit entirely inside the calling token's tag
restriction. If even one of them falls outside it, the whole call is
refused; there is no partial run against just the servers the token happens
to be able to see. A token scoped to `env=staging` cannot use a
production-and-staging workflow to touch only the staging half - it has to
be pointed at a workflow whose saved targets are staging-only from the
start.
## What an agent can create
Three tools let an agent propose new work rather than only look at existing
work: `create_step`, `create_workflow` and `create_monitor`. Nothing they
create is armed:
- A created **workflow** cannot be given a schedule. `create_workflow`
refuses outright if a `schedule` argument is present at all, so scheduling
stays a separate, later decision a human makes in the UI.
- A created **monitor** is always saved disabled, regardless of any
`enabled` argument. It checks nothing and alerts nobody until a human
turns it on.
- A created **step** may not reference a secret. `create_step` refuses a
`secret_refs` argument, because a script an agent wrote is not something
you want run with credentials attached before a person has read it.
`create_workflow` applies the same two checks `run_workflow` applies before
a run: it refuses to save a workflow whose targets are not entirely inside
the token's tag scope, and it refuses (absent `confirm:true`) to save one
whose targets exceed the 25-server fan-out limit. A workflow this token
could not later run is never created in the first place.
Steps an agent creates are marked with a `source` of `mcp`, which the
Vantage UI uses to badge them, so a human reviewing the step library can
tell at a glance which scripts came from an agent rather than a person.
There is no tool to update or delete an existing step, workflow or monitor.
An agent can add to your instance; it cannot change or remove anything that
was already there.
## What an agent cannot do
This is the section to read before turning the feature on.
- **It cannot see a secret's plaintext.** `list_secret_names` returns group
and key names only; no tool anywhere in the MCP surface returns a
secret's value to a model.
- **It cannot open a console or a shell.** There is no MCP tool for the
browser console, SSH, RDP or VNC. An agent can trigger a workflow step
that runs a script, but it cannot open an interactive session on a
machine.
- **It cannot exceed its tag restriction.** Every tool that resolves
servers - by ID, by tag, or through a saved workflow - resolves them
through the token's own scope. A server outside that scope is invisible
to `list_servers` and `get_server`, not-found to anything asking about it
by ID, and excluded from what a write tool will act on.
- **It cannot act at all without `mcp:write`.** Every write tool needs it,
on top of its own resource scope, and a token that lacks it never even
sees those tools in the listing.
- **It cannot touch more than 25 servers in one call without saying so
explicitly.** Any write tool that would affect more than 25 servers is
refused with a message asking for `confirm:true` before it will proceed -
cheap insurance against a mis-parsed tag selector reaching the whole
fleet on one badly phrased instruction.
- **It cannot edit or delete anything that already exists.** The creation
tools only add; there is no MCP tool that updates or removes a step,
workflow or monitor a person defined.
A monitor whose runner server sits outside a token's scope is not hidden
from that token - it still appears in `list_monitors`. The REST API reaches
that outcome by redacting: it replaces the runner with a
`RunnerRestricted` placeholder when the runner falls outside the caller's
visible servers. The MCP tools reach the same outcome a different way -
`list_monitors` and `get_monitor_status` project a monitor onto a summary
that never carries a runner field for any caller, restricted or not, so
there is nothing there to redact in the first place. Two mechanisms, chosen
so the two surfaces cannot end up disagreeing about what a restricted token
learns. A workflow's target list is filtered more directly: servers outside
the token's scope are dropped from what `get_workflow` reports, and a
`targets_restricted` flag says that some were held back, without saying how
many.
## Every call is audited
Every tool call reaches the audit log, whether it read something or changed
something:
- A successful call - read or write - is recorded as `mcp.tool_call`.
- A call refused by policy - missing scope, an out-of-scope target, or a
fan-out over 25 servers without confirmation - is recorded as
`mcp.tool_denied`, naming the specific gate that refused it, so a human
reading the log can tell "the agent was stopped" from "the agent tried
and the underlying action failed."
- A write tool that reached the underlying service and that service
rejected is recorded as `mcp.tool_failed`.
- Creating a step, workflow or monitor is recorded separately as
`mcp.created`, naming what was made and its ID - a question like "what
has this agent added to my instance" should not require digging through
hundreds of read rows to answer.
See the [audit log](./audit-log.md) for how to read these alongside
everything else on the instance.
+111
View File
@@ -0,0 +1,111 @@
---
id: metric-alerts
title: Metric alerts
sidebar_label: Metric alerts
---
Agents already report disk, memory, load, reboot status and the state of every
container and systemd unit. A metric alert turns those reports into incidents:
"disk above 90% on any production server", "a systemd unit failed", "a reboot
has been pending for more than 7 days".
One rule covers as many servers as its tags match. There is nothing to install:
alerts use what agents already send, on any agent version.
## Creating a rule
1. Go to **Monitors** and choose **New monitor**.
2. Pick **Server metric**.
3. Under **Servers**, choose which servers by tag. Leave it empty to watch every server.
4. Choose the **Metric** (the condition below) and its **Threshold**.
5. Set **For (minutes)**: how long the condition must hold before it alerts.
6. Attach [notification channels](./notification-channels.md) and save.
Tags are resolved each time the rule is checked, so a server tagged `env=prod`
tomorrow is covered from then on, and a server whose tag is removed stops being
watched.
## Conditions
| Metric | Alerts when | Threshold |
| --------------------- | ---------------------------------------------------------- | --------- |
| Disk used | Used space is at or above the threshold | percent |
| Disk free below | Free space is at or below the threshold | GB |
| Memory used | Used memory is at or above the threshold | percent |
| Load per core | 1-minute load divided by CPU cores is at or above it | ratio |
| Systemd unit failed | Any unit on the server is `failed` | none |
| Container unhealthy | Any container's health check reports `unhealthy` | none |
| Reboot pending for | The server has needed a reboot for at least this long | days |
| Agent offline for | The agent has not been seen for at least this long | minutes |
The two disk metrics take an optional **Mount**, such as `/var`. Without one,
every mount is checked and the fullest one is reported.
## How a rule decides
Rules are checked every 30 seconds, against each matching server separately.
- When a server first meets the condition, it becomes **pending**.
- If the condition is still met after **For (minutes)**, the server goes
**down**, an incident opens for that server, and the channels fire.
- As soon as the condition clears, the server is **up** and its incident
closes. A pending server that clears never alerts.
"For" is continuous: a server that drops below the threshold for one check
starts the count again. Use it to ignore a nightly backup filling a disk for
ten minutes, or a load spike during a deploy. Set it to 0 to alert on the first
check.
### Per-server incidents
Each server has its own state and its own incident. If a rule matches 40
servers and 3 are breaching, you get 3 incidents, and each alert names its
server:
```text
[Vantage] Disk full (metric) on web-01 is DOWN: /var 94.2% used
```
The monitors list shows the rule as a whole, for example
`3 of 40 servers breaching`. The rule's own page lists every matching server
with its state, the current value and how long it has been breaching.
Webhook payloads carry a `server_name` field for metric alerts; see
[notification channels](./notification-channels.md#webhook).
## Stale data
Agents send metrics every 30 seconds. If a server's last metrics are more than
5 minutes old, the rule skips that server and keeps its previous state. A
powered-off server does not suddenly clear, or trip, a disk alert.
To be told about the server itself going quiet, add an **Agent offline for**
rule. It is the one metric that does not need fresh reports.
Some reports arrive less often:
- **Reboot pending** is refreshed with the agent's full inventory, every 15
minutes and at agent start.
- **Units and containers** are reported every 60 seconds.
## When servers leave a rule
If a server stops matching (its tag changed, or it was deleted) while it has an
open incident, the incident is closed quietly, with no recovery message. Nothing
recovered; the server simply stopped being watched.
## Tag-restricted API keys
An [API key restricted to tags](../reference/api-tokens.md#tag-restrictions) can
only create a metric rule whose tags include all of its own. A key restricted
to `env=staging` can create a rule for `env=staging role=web`, but not one for
every server, and not one for `env=prod`.
The same key cannot rename, disable, change or delete an existing rule that
reaches further than its tags, and it only sees incidents and per-server states
for servers inside them.
## Status pages
A metric rule can be added to a [status page](./status-pages.md) like any other
monitor. Its uptime reflects whether any matching server was down.
+14 -2
View File
@@ -4,8 +4,11 @@ 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.
Monitors check that something is answering. Four check types, two places they
can run from, and a notification path when they stop being satisfied. Two more
types watch for things that don't answer a check:
[heartbeat monitors](./heartbeat-monitors.md) and
[metric alerts](./metric-alerts.md).
## Types
@@ -19,6 +22,15 @@ from, and a notification path when they stop being satisfied.
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.
| Type | Watches | Guide |
| ----------- | --------------------------------------------------------------------------- | ------------------------------------------- |
| `heartbeat` | A job that calls a Vantage URL when it runs; alerts when the call stops | [Heartbeat monitors](./heartbeat-monitors.md) |
| `metric` | Disk, memory, load, units, containers, reboots and agents on tagged servers | [Metric alerts](./metric-alerts.md) |
These two have no runner, interval or retries. The rest of this page is about
the four check types; state, incidents, notifications and uptime work the same
for all six.
## Where a check runs
Every monitor has a **runner**:
+120
View File
@@ -0,0 +1,120 @@
---
id: multi-factor-authentication
title: Multi-factor authentication
sidebar_label: Multi-factor authentication
---
A second sign-in factor for password accounts, available on every plan, no
licence required. Two kinds: an authenticator app (TOTP) and a passkey.
:::info Not for single sign-on accounts
If you sign in through your organisation's identity provider, two-factor
authentication and passkeys are set up there, not in Vantage. Your provider's
own session policy applies instead.
:::
## Setting up an authenticator app
1. Go to your account menu and choose **Security**.
2. Under **Authenticator app**, choose **Set up**.
3. Scan the QR code with an app such as Google Authenticator, 1Password or
Authy, or enter the shown key manually if you cannot scan.
4. Enter the 6-digit code the app displays to confirm it.
Once confirmed, you will be asked for a fresh code from that app every time
you sign in with your password.
## Setting up a passkey
A passkey uses your device's built-in security (a fingerprint, face
recognition, or a security key) instead of a code. It can be used two ways:
as a second factor after your password, or on its own for **passwordless**
sign-in.
1. Go to **Security** in your account menu.
2. Under **Passkeys**, choose **Add a passkey**.
3. Follow your browser or device's prompt.
Once added, the login page offers a **Sign in with passkey** button that
needs no password at all, alongside the usual second-factor prompt if you
sign in with a password instead.
You can add more than one passkey (for example, one per device) and rename or
remove them individually from the Security page.
## Recovery codes
The first time you set up either factor, Vantage shows you ten **recovery
codes**. Each one works once, in place of your authenticator app or passkey,
if you lose access to both. Save them somewhere safe - a password manager or
a printed copy - because they are shown only this one time.
If you run low, regenerate a fresh batch of ten from the Security page. This
immediately invalidates every code from the previous batch.
:::warning Losing every factor and every recovery code
If you lose your authenticator app, your passkeys and your recovery codes all
at once, you cannot sign yourself back in. An owner or admin can reset your
MFA from **Settings → People** (see below), after which you can sign in with
your password and set up a new factor.
:::
## Owners: requiring MFA for everyone
Owners can turn on **Require MFA for password sign-in** under **Settings →
Access**. When this is on:
- Anyone signing in with a password who has not yet set up a factor is asked
to enrol one immediately, before they can do anything else.
- Anyone who already has a factor is unaffected beyond the normal prompt.
- Members who sign in through single sign-on, or with a passkey used
passwordlessly, already satisfy the requirement and are not interrupted.
- People already signed in are not signed out. The requirement applies from
their next sign-in.
- Nobody who is required to have a factor can remove their last one - the
**Remove** buttons on the Security page are disabled once removing them
would leave the account with none.
## Resetting a locked-out member's MFA
If a member loses access to their authenticator app, their passkeys and their
recovery codes, an owner or admin can clear their MFA entirely:
1. Go to **Settings → People**.
2. Find the member and choose **Reset MFA**.
3. Confirm your own identity when prompted (this is a sensitive action, so it
asks you to re-authenticate first).
This removes their authenticator app, every passkey and every recovery code.
They sign in with their password alone and are asked to set up a new factor
on their next sign-in, or immediately if **Require MFA** is on.
An admin cannot reset an owner's MFA - only another owner can.
## Passkeys and moving your instance
A passkey is tied to the exact address you registered it on. If you later
move a self-hosted instance to a new domain, or rename a cloud instance so
its address changes, every passkey registered on the old address stops
working - your browser and device will not offer them for a different host,
by design of the WebAuthn standard itself.
Authenticator app codes and recovery codes are unaffected by a host change,
since neither is bound to an address. If you rely on passkeys, plan to
re-register them after moving or renaming an instance, and keep your recovery
codes handy in the meantime.
## Re-confirming your identity for sensitive actions
A handful of actions ask you to confirm your identity again even while
signed in, whether or not you have MFA enrolled: revealing a vault secret,
downloading a private key, and connecting to the browser console. This
confirmation (using your factor, or your password if you have none) is valid
for ten minutes, so you are not asked again for a second sensitive action
shortly after the first.
:::info API tokens are not prompted
An API token performs these same actions with no re-confirmation step, since
there is no person present to prompt. If this matters for your use case,
issue narrowly scoped, short-lived tokens rather than broad ones.
:::
+4 -1
View File
@@ -30,6 +30,9 @@ Posts JSON to a URL you choose.
}
```
Alerts from a [metric alert](./metric-alerts.md) add `"server_name"` with the
server that breached. Other monitor types leave it out.
Any response of 300 or above counts as a delivery failure. The request times out
after 10 seconds.
@@ -67,7 +70,7 @@ Port `465` uses implicit TLS; anything else uses STARTTLS.
### Credentials are never read back
The SMTP `password`, the Telegram `token` and the webhook, Slack and Discord
`url`s come back from `GET /api/channels` as `••••••••` a webhook URL is the
`url`s come back from `GET /api/channels` as `••••••••` - a webhook URL is the
authorisation to post to that channel, so it is treated as a credential like
the rest. Writing that value back unchanged keeps the stored one, which is what
lets you rename a channel without retyping its password. Anything else you send
+121
View File
@@ -0,0 +1,121 @@
---
id: patching
title: Patching
sidebar_label: Patching
---
Patching installs OS updates on your servers inside a **maintenance window**
you choose, and records what happened on every server. It is available on every
tier: security patching is never a paid feature.
Three things work together:
| Thing | What it answers |
| ----------------------- | ----------------------------------------------------------- |
| A maintenance window | *When.* "Sundays 02:00 to 04:00, Europe/London" |
| A patch policy | *What and where.* "Security updates on every `env:prod` server, reboot if needed" |
| A patch run | *What happened.* One record per window, with a result per server |
Clicking **Apply updates** on a server or on the vulnerabilities page also
creates a run, so every patch Vantage performs has a record.
## Maintenance windows
**Patching → Windows → New window.** A window has a name, a start time written
as five-field cron, a timezone and a length from 15 minutes to 12 hours. The
editor shows the next three windows, computed by the same code that opens
them.
The timezone is stored by name, so a 02:00 window stays at 02:00 across
daylight-saving changes. A window never starts while the previous one is still
open, including on the night the clocks go back and 01:30 happens twice.
A window used by a policy cannot be deleted. Move the policy to another window
first.
## Patch policies
**Patching → Policies → New policy.** Owners and admins can create policies.
| Setting | Meaning |
| -------------- | ------- |
| Window | The maintenance window the policy runs in |
| Targets | Named servers, tags, or both, exactly as for [workflows](./workflows.md#targeting). Tags are read when the window opens |
| What to install | **Security updates only** or **All pending updates** |
| Reboots | **Never reboot**, or **Reboot if required** |
| At most this many at once | How many servers patch at the same time. 0 means no limit |
| Alert channels | Told when a run finishes with anything other than every server succeeding |
**Run now** opens a window of the policy's usual length starting immediately.
It is the way to try a policy before trusting it with a Sunday.
### Security updates only
| Package manager | How security-only works |
| --------------- | ----------------------- |
| apt (Debian, Ubuntu) | Only your `-security` sources are used |
| dnf, yum (RHEL, Rocky, Alma, Fedora) | `--security` |
| zypper (SUSE) | Security patches only |
| Windows | The Security Updates and Critical Updates classifications |
| apk (Alpine), pacman (Arch) | **Not supported.** These publish no security metadata, so the server reports *unsupported* and nothing is installed |
Security-only never falls back to installing everything.
### Reboots
With **Reboot if required**, a server reboots only when its OS reports that a
reboot is owed, and only if at least 5 minutes of the window remain. The agent
reports first, then reboots after one minute.
Vantage then waits for the server to come back. The reboot counts as done when
the agent reports a boot time later than the reboot, with no reboot still
owed. A server that does not come back within 45 minutes is marked failed.
With **Never reboot**, the server shows **reboot required** instead.
## What happens during a window
- Servers start patching as the window opens, up to the concurrency limit.
- A server whose agent is offline is retried while the window is open.
- No server starts patching in the last 15 minutes of a window. Servers still
waiting then are marked when the window closes.
- Servers already patching are allowed to finish, even past the window end
(up to 2 hours from when each started). Interrupting a package manager is
worse than letting it finish late.
- A reboot only happens if at least 5 minutes of the window remain.
- A policy whose previous run is still going skips the window, and says so on
the policy.
## Patch runs
**Patching → Runs** lists every run. Open one to see each server's result, how
many updates were installed, reboot times, and the last part of the package
manager's output.
| Server status | Meaning |
| ------------- | ------- |
| queued | Waiting for a concurrency slot |
| waiting for agent | The agent is offline; retried while the window is open |
| patching | Installing now |
| rebooting | Rebooted; waiting for it to come back |
| succeeded | Patched, and rebooted and back if a reboot was owed and allowed |
| failed | The package manager failed, the agent did not answer, or the reboot did not complete |
| unsupported | Security-only on a server with no security metadata |
| agent too old | The agent must be updated before it can take part |
| missed, offline | Offline for the whole window |
| window closed | Still waiting when the window ended |
| cancelled | The run was cancelled before this server started |
A run is **succeeded** when every server succeeded, **failed** when none did,
and **partial** otherwise. Anyone can cancel a running run: servers already
patching finish, and nothing further starts.
Runs are kept for the same time as workflow logs (**Settings → Monitoring**).
## Agent version
Patch policies need agent **1.4.0** or later. An older agent would ignore
"security only" and install everything, so Vantage does not send it policy
work: it shows **agent too old** until you update it
(see [Agent updates](../operations/agent-updates.md)). **Apply updates** still
works on an older agent, but the run cannot report a result.
+9 -9
View File
@@ -89,20 +89,20 @@ metrics is normal rather than a fault.
### OS updates
Agents check for pending package updates hourly and report the count the
Agents check for pending package updates hourly and report the count - the
machine's own package manager on Linux, the Windows Update COM API on Windows.
From the server page you can:
- **Apply updates** runs that check's install path and reports back. The agent
never reboots the machine; if one is owed, a **reboot required** badge
appears on the next inventory snapshot instead.
- **Apply updates** installs every pending update now, without rebooting, and
opens the [patch run](./patching.md#patch-runs) recording the result. If a
reboot is owed, a **reboot required** badge appears on the next inventory
snapshot.
- **Update agent** upgrades the Vantage agent on that machine. See
[Agent updates](../operations/agent-updates.md).
:::warning Applying updates is not scheduled or staged
It runs immediately, on that machine. If you need ordering, health checks or a
test machine first, build it as a [workflow](./workflows.md) instead.
:::
The panel also shows which [patch policy](./patching.md) covers the server and
when its next window opens. To patch on a schedule, security-only, or with
reboots, use a patch policy.
### Console
@@ -113,7 +113,7 @@ Opens a browser SSH, RDP or VNC session. See [Browser console](./browser-console
Windows agents register, heartbeat, run workflow steps, report inventory,
check and apply OS updates, report workloads (services and containers), and
serve the browser console. They do not manage `authorized_keys`, and they are
not covered by package inventory or CVE scanning the vulnerability feeds
not covered by package inventory or CVE scanning - the vulnerability feeds
this project uses carry no Windows data.
## Removing a server
+6
View File
@@ -78,6 +78,12 @@ sign-in. Vantage refuses any change that would leave nobody able to sign in,
whether that is switching off passwords or disabling your last provider. Keep
one route open until everyone who needs access can use the new one.
### Require MFA
Owners can require a second sign-in factor for everyone signing in with a
password. See [Multi-factor authentication](multi-factor-authentication.md)
for what this does, how members enrol, and how to reset a locked-out member.
## Monitoring
- **Offline threshold**, how long a server may go unheard from before it is
+15 -15
View File
@@ -6,12 +6,12 @@ sidebar_label: Status pages
A status page is a public page reporting a chosen set of monitors as up-front
components, with a 90-day history and an uptime percentage per component. It
needs no session and no token to read anyone with the link can open it,
needs no session and no token to read - anyone with the link can open it,
which is the point: it is what you hand a customer instead of an incident
email.
Requires the **Status pages** licence feature. If the licence lapses, or the
tier does not include the feature, the page keeps serving it renders an
tier does not include the feature, the page keeps serving - it renders an
explanation rather than data or a broken page, so a customer who follows an
old link never sees an error.
@@ -28,7 +28,7 @@ https://<your-vantage-address>/status/<page-id>
On **Vantage Cloud** that address is your instance's own subdomain, so the page
is at `https://<your-instance>.vantage.hostxtra.co.uk/status/<page-id>`.
On a **self-hosted** install it is whatever address you reach Vantage on
On a **self-hosted** install it is whatever address you reach Vantage on -
`https://vantage.acme.com/status/<page-id>`, or an IP and port on a LAN
install. A self-hosted install serves exactly one Vantage instance, so no
subdomain is needed to say which one you mean. The **Copy** control next to the
@@ -37,26 +37,26 @@ the one to hand out.
**The page id cannot be changed after creation.** Once you have shared the
link, changing the id would break it, so pick something you would still be
happy with in a year `platform`, `api`, a customer's own name for a
happy with in a year - `platform`, `api`, a customer's own name for a
dedicated page.
## Draft versus published
A new page starts unpublished. Unpublished pages answer *not found* to
anyone who requests them, including you, from a browser without a session
anyone who requests them, including you, from a browser without a session -
so you can build out the components and copy before announcing it. Toggle
**Published** when it is ready. Un-publishing later takes it back to *not
found* rather than deleting anything.
**Delete page**, in the editor header, is the only way to correct a page id you
regret the id is fixed once created. It takes the page, its sections and its
regret - the id is fixed once created. It takes the page, its sections and its
authored incidents with it; monitors and their history are untouched. If you
only want the page off the internet, un-publish it instead.
## Sections and components
A page is organised into **sections** arbitrary groupings such as "API" or
"Region: EU" each holding one or more **components**. A component is a
A page is organised into **sections** - arbitrary groupings such as "API" or
"Region: EU" - each holding one or more **components**. A component is a
monitor plus a **display name** you choose for this page.
The display name is never the monitor's own name unless you type it in. An
@@ -64,7 +64,7 @@ internal monitor name ("prod-db-primary-eu1") is rarely what you want a
customer reading; give it whatever name makes sense to them, and change it
for a different page without touching the monitor.
If a monitor listed on a page is later deleted, its component still appears
If a monitor listed on a page is later deleted, its component still appears -
reading `Unknown` rather than up or down, because nothing is checking it any
more and claiming otherwise would be a false claim of health.
@@ -77,7 +77,7 @@ more and claiming otherwise would be a false claim of health.
- A 90-day history bar per component.
- Any active incidents, upcoming maintenance, and a rolling history of both.
- An optional banner across the top of the page, for anything you want said
regardless of component state. It is one notice with one appearance there
regardless of component state. It is one notice with one appearance - there
are no severity levels to choose between.
A visitor never sees a target URL, host or port, the check's expected status
@@ -90,17 +90,17 @@ reachable is on this page.
Two kinds of entries appear on a page's timeline:
- **Automatic** a monitor going down opens an incident on any page that
- **Automatic** - a monitor going down opens an incident on any page that
lists it, with no action from you. These appear the moment the monitor's
state changes and close the moment it recovers.
- **Authored** an incident or maintenance window you create by hand, with
- **Authored** - an incident or maintenance window you create by hand, with
its own title, impact and a set of affected components you choose. You
post updates to it (Investigating → Identified → Monitoring → Resolved) as
the situation develops, and each update is timestamped and kept on the
page's history.
An authored incident is attached to one or more pages explicitly when you
create it it does not follow a monitor onto every page that monitor happens
create it - it does not follow a monitor onto every page that monitor happens
to be listed on.
### Scheduling maintenance
@@ -111,7 +111,7 @@ is in progress and its affected components are within the scheduled time,
those components are drawn as "under maintenance" instead of up or down.
**Maintenance changes how a day is drawn, never the uptime number itself.**
The 90-day percentage is computed from what actually happened a component
The 90-day percentage is computed from what actually happened - a component
that stayed up throughout a maintenance window still shows as up in its
history, it is only the live status pill that reads "under maintenance" for
the duration.
@@ -120,6 +120,6 @@ the duration.
A visitor's read of a page is cached for up to 30 seconds, so posting an
update or flipping Published does not necessarily change what a visitor sees
instantly though most authoring actions invalidate that cache immediately,
instantly - though most authoring actions invalidate that cache immediately,
so in practice it usually shows within a second or two. If a change genuinely
does not appear, reloading after 30 seconds always will.
+5 -5
View File
@@ -55,12 +55,12 @@ against three-week-old data is not the same as a low count.
## Fixing something
A finding with a known fixed version gets an **Apply updates** button, which
runs the same OS update the server page offers. There is no separate patching
mechanism.
A finding with a known fixed version gets an **Apply updates** button. It
installs every pending update on that server now and opens the
[patch run](./patching.md#patch-runs) so you can see the result.
Vantage never patches automatically. Applying updates is always something you
ask for.
To keep servers patched without clicking, create a
[patch policy](./patching.md) with **Security updates only**.
## Accepting a finding
+1 -1
View File
@@ -4,7 +4,7 @@ title: Workloads
sidebar_label: Workloads
---
A **workload** is one Docker container or one service a systemd unit on
A **workload** is one Docker container or one service - a systemd unit on
Linux, a Windows service on Windows. Every server reports what it is running,
and you can start, stop and restart those workloads, and read their recent
logs, without opening a console.
+1 -1
View File
@@ -11,7 +11,7 @@ server {
return 302 /docs/;
}
# Hashed assets are immutable the filename changes when the content does.
# Hashed assets are immutable - the filename changes when the content does.
location /docs/assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
+5
View File
@@ -26,12 +26,17 @@ const sidebars: SidebarsConfig = {
"vantage/ssh-keys",
"vantage/workflows",
"vantage/monitors",
"vantage/heartbeat-monitors",
"vantage/metric-alerts",
"vantage/vulnerabilities",
"vantage/patching",
"vantage/workloads",
"vantage/notification-channels",
"vantage/status-pages",
"vantage/secrets",
"vantage/browser-console",
"vantage/multi-factor-authentication",
"vantage/mcp",
"vantage/audit-log",
"vantage/settings",
],
+3 -3
View File
@@ -1,7 +1,7 @@
/* ==========================================================================
Vantage documentation design tokens
The token block below is COPIED VERBATIM from site/app/globals.css same
The token block below is COPIED VERBATIM from site/app/globals.css - same
names, same values. adminsite/ holds a copy too, web/ holds the dark half,
and shared/mail/templates/layout.html.tmpl holds it a fifth time as literal
hex because email clients support neither var() nor prefers-color-scheme.
@@ -13,7 +13,7 @@
value outside the two token blocks.
Docusaurus always stamps data-theme on <html>, so unlike site/ there is no
prefers-color-scheme branch to keep in step the theme toggle is the only
prefers-color-scheme branch to keep in step - the theme toggle is the only
writer.
========================================================================== */
@@ -200,7 +200,7 @@
color: var(--ink);
}
/* Machine output install one-liners, key blobs, run logs sits on a floor
/* Machine output - install one-liners, key blobs, run logs - sits on a floor
beneath the panel, the same distinction web/ draws with --well. */
.theme-code-block {
border: 1px solid var(--rule-soft);
+1 -1
View File
@@ -1,7 +1,7 @@
<!--
The Vantage mark, traced from site/components/Logo.tsx. A favicon is an asset
rather than a component, and a browser tab has no access to the token block,
so the logo navy is a literal here the same concession the email layout
so the logo navy is a literal here - the same concession the email layout
makes. Keep it in step with --logo.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="246 207 533 610">

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB