Compare commits
23
Commits
d69a36ed7b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53629450e0 | ||
|
|
f230b66384 | ||
|
|
a1960b26ea | ||
|
|
d5e5377fae | ||
|
|
4623d9b7b0 | ||
|
|
58a5230405 | ||
|
|
50b5ee2964 | ||
|
|
7762629634 | ||
|
|
e8455c99fe | ||
|
|
fbfb4b8608 | ||
|
|
fcedb12f39 | ||
|
|
a0c20299a5 | ||
|
|
c4bc4b68bf | ||
|
|
f7eee4fc3e | ||
|
|
cca6453b86 | ||
|
|
aa581b5d1e | ||
|
|
f41f2aaae7 | ||
|
|
76057eb346 | ||
|
|
f564dd0479 | ||
|
|
54671a3652 | ||
|
|
8359791b6e | ||
|
|
036875345a | ||
|
|
c6cd96cbd1 |
@@ -0,0 +1,45 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
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
|
||||
# escape hatch for that, and DOCS_BASE_URL in particular is baked into
|
||||
# every asset path in the build.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-docker
|
||||
container: docker:dind
|
||||
steps:
|
||||
- name: Setup
|
||||
run: apk add --update nodejs npm git
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to registry
|
||||
run: |
|
||||
echo "${{ secrets.RELEASE_TOKEN }}" | \
|
||||
docker login ${{ vars.DOCKER_HOST }} \
|
||||
-u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
|
||||
# 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.
|
||||
#
|
||||
# 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. DOCS_BASE_URL in particular must match the proxy
|
||||
# location that routes here and the directory the runtime stage
|
||||
# serves from; when those three disagree the HTML loads and every
|
||||
# asset 404s. Change it in the Dockerfile, not here.
|
||||
- name: Build and push image
|
||||
run: |
|
||||
IMAGE="${{ vars.DOCKER_HOST }}/vantage/vantage-docs:latest"
|
||||
docker build -t "$IMAGE" .
|
||||
docker push "$IMAGE"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
.env
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Vantage documentation (`vantage-docs`)
|
||||
|
||||
The user-facing documentation, served at **vantage.hostxtra.co.uk/docs**.
|
||||
Docusaurus 3 in docs-only mode (`routeBasePath: "/"`, no blog), one version
|
||||
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.
|
||||
|
||||
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
|
||||
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` -
|
||||
so a `docs.` label would be read as a tenant slug. Hence `/docs` on the
|
||||
marketing host instead.
|
||||
|
||||
**Three things must agree, and when they do not the HTML loads while every
|
||||
asset 404s:**
|
||||
|
||||
1. `DOCS_BASE_URL` (default `/docs/` in the Dockerfile)
|
||||
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`
|
||||
|
||||
The proxy forwards the **full** path; it does not strip `/docs`. That is the
|
||||
whole reason the third item exists.
|
||||
|
||||
Unlike the three Next apps this builds to static files, so its runtime stage is
|
||||
`nginx:alpine-slim` rather than Node, and it listens on **80**, not 3000.
|
||||
|
||||
## 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
|
||||
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
|
||||
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.
|
||||
|
||||
**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
|
||||
no way to make them one commit.
|
||||
|
||||
## Deployment
|
||||
|
||||
A fragment, composed on top of the control plane's own file:
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
-f vantage/deploy/docker/docker-compose.yml \
|
||||
-f vantage-site/deploy/docker-compose.yml \
|
||||
-f vantage-docs/deploy/docker-compose.yml \
|
||||
-f vantage-admin/deploy/docker-compose.yml \
|
||||
up -d
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
`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.
|
||||
|
||||
| Name | Type | Value |
|
||||
| --------------- | ------ | ----------------------------------------------------------- |
|
||||
| `RELEASE_TOKEN` | Secret | Gitea API token, `write:package`. No module fetch here, so it needs no org read access |
|
||||
| `REGISTRY_USER` | Secret | Gitea username. Must own `RELEASE_TOKEN` |
|
||||
| `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.
|
||||
+2
-2
@@ -25,9 +25,9 @@ RUN npm run build
|
||||
|
||||
# Runtime stage
|
||||
#
|
||||
# Docusaurus emits a fully static site, so unlike web/, site/ and adminsite/
|
||||
# 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
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# The user documentation, served at vantage.hostxtra.co.uk/docs.
|
||||
#
|
||||
# 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
|
||||
# the per-tenant instance namespace, and the control plane's APP_ROOT_LABEL
|
||||
# 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
|
||||
# image serves from (/usr/share/nginx/html/docs) must all agree. When they do
|
||||
# not, the HTML loads and every asset 404s.
|
||||
#
|
||||
# Unlike the Next apps this is a static build behind nginx, so it listens on 80
|
||||
# rather than 3000.
|
||||
services:
|
||||
docsite:
|
||||
image: gitea.hostxtra.co.uk/vantage/vantage-docs:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 3005:80
|
||||
networks: {}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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,10 +90,149 @@ 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
|
||||
|
||||
Point your reverse proxy at `web` on port `3000` and terminate TLS there. The
|
||||
web app reaches the API internally, so there is no need to publish port `8080`.
|
||||
The `nginx` service routes **one hostname to two backends**:
|
||||
|
||||
| Path | Backend |
|
||||
| -------------------------------------------------------------------------------- | ------------- |
|
||||
| `/api`, `/auth`, `/public`, `/install`, `/install.ps1`, `/update`, `/update.ps1` | `server:8080` |
|
||||
| everything else | `web:3000` |
|
||||
|
||||
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. 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
|
||||
@@ -105,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.
|
||||
@@ -120,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
@@ -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.
|
||||
|
||||
@@ -28,31 +28,22 @@ entitlement.
|
||||
|
||||
## Features
|
||||
|
||||
Three features are enabled per instance rather than bundled into a tier:
|
||||
Four features are enabled per instance rather than bundled into a tier:
|
||||
|
||||
| Feature | What it enables |
|
||||
| ---------------------- | ------------------------------------------------------------------------------- |
|
||||
| Browser console | The [browser console](../vantage/browser-console.md) |
|
||||
| Single sign-on | [Sign-in through your identity provider](../vantage/settings.md#single-sign-on) |
|
||||
| Vulnerability scanning | [Package vulnerability scanning](../vantage/vulnerabilities.md) |
|
||||
| Status pages | [Public status pages](../vantage/status-pages.md) |
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
---
|
||||
id: backup-and-restore
|
||||
title: Backup and restore
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
## What it does not hold
|
||||
|
||||
- **Redis sessions.** Everyone signs in again after a restore, which is already
|
||||
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.
|
||||
|
||||
:::note Pin the version
|
||||
The image is published on each `vantagectl/v*` release and tagged with that
|
||||
version; `:latest` also moves. Pin a version in anything scheduled. A restore
|
||||
is easier to reason about when you can say which build produced the archive and
|
||||
which one read it back.
|
||||
:::
|
||||
|
||||
## Taking a backup
|
||||
|
||||
The loose binary:
|
||||
|
||||
```bash
|
||||
export MONGO_URI=mongodb://localhost:27017
|
||||
export MONGO_DB=vantage
|
||||
export KEY_ENCRYPTION_KEY=<your 64-char hex key>
|
||||
vantagectl backup --out /backups
|
||||
```
|
||||
|
||||
The container:
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-e MONGO_URI=mongodb://mongo:27017 \
|
||||
-e MONGO_DB=vantage \
|
||||
-e KEY_ENCRYPTION_KEY=<your 64-char hex key> \
|
||||
-v /backups:/backups \
|
||||
gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:0.1.0 backup --out /backups
|
||||
```
|
||||
|
||||
Kubernetes, as a scheduled `CronJob` the Helm chart can render for you:
|
||||
|
||||
```yaml
|
||||
backup:
|
||||
enabled: true
|
||||
schedule: "0 2 * * *"
|
||||
image: "gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:0.1.0"
|
||||
pvcName: "vantage-backups"
|
||||
```
|
||||
|
||||
`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
|
||||
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
|
||||
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
|
||||
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
|
||||
else is always safe, nothing progress-related lands in the stream.
|
||||
|
||||
Into `restic`:
|
||||
|
||||
```bash
|
||||
vantagectl backup --out - | restic backup --stdin --stdin-filename vantage.tar.gz
|
||||
```
|
||||
|
||||
Into S3:
|
||||
|
||||
```bash
|
||||
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
|
||||
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.
|
||||
|
||||
## Checking a backup is real
|
||||
|
||||
```bash
|
||||
vantagectl verify /backups/vantage-backup-vantage-20260907T020000Z.tar.gz \
|
||||
--mongo-uri mongodb://localhost:27017 --db vantage
|
||||
```
|
||||
|
||||
Each line of output answers a different question:
|
||||
|
||||
- **`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
|
||||
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
|
||||
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
|
||||
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
|
||||
`--mongo-uri`, no key. It is what to run against an archive of unknown origin,
|
||||
before deciding whether it is the one you want:
|
||||
|
||||
```bash
|
||||
vantagectl inspect /backups/vantage-backup-vantage-20260907T020000Z.tar.gz
|
||||
```
|
||||
|
||||
It reports when the archive was taken and on which host, the Vantage and
|
||||
MongoDB versions behind it, the database it came from, the key fingerprint (or
|
||||
that it carries none), every collection with its document count and size, and
|
||||
anything `--exclude` left out. Opening the archive verifies every member's
|
||||
checksum on the way, so a corrupt archive fails here too.
|
||||
|
||||
Reach for `verify` instead when the question is whether the key you hold opens
|
||||
it; reach for `inspect` when the question is what it is.
|
||||
|
||||
## Restoring
|
||||
|
||||
`restore` expects the target database to be empty. Pointed at one that already
|
||||
holds data, it refuses outright: there are no merge semantics, because merging
|
||||
two control planes reconciles nothing and upserting old data over new would
|
||||
resurrect revoked keys and deleted users.
|
||||
|
||||
```bash
|
||||
vantagectl restore /backups/vantage-backup-vantage-20260907T020000Z.tar.gz \
|
||||
--mongo-uri mongodb://localhost:27017 --db vantage_restore
|
||||
```
|
||||
|
||||
To overwrite a database that is not empty, add `--force`, which drops each
|
||||
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.
|
||||
- Nothing, on a terminal: `--force` alone prompts you to type the target
|
||||
database's name back, a deliberate pause before something destructive.
|
||||
|
||||
Without a terminal and without `--confirm-db`, `--force` is refused: there is
|
||||
nobody there to prompt. Naming the database in the command itself means a
|
||||
copy-pasted invocation carries its intended target with it and cannot destroy a
|
||||
different one by accident.
|
||||
|
||||
`--force` drops only the collections the archive carries. Anything else already
|
||||
in the target is left alone and named in a warning, so an archive taken with
|
||||
`--exclude workflow_log_lines` restored over a live database tells you the old
|
||||
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.
|
||||
|
||||
## The restore drill
|
||||
|
||||
An untested backup is a hypothesis, not a backup. Rehearse the whole path,
|
||||
monthly:
|
||||
|
||||
1. Restore last night's archive into a scratch database:
|
||||
```bash
|
||||
vantagectl restore /backups/vantage-backup-vantage-<date>.tar.gz \
|
||||
--mongo-uri mongodb://localhost:27017 --db vantage_drill
|
||||
```
|
||||
2. Run `verify` against the result to confirm the data that landed is actually
|
||||
readable with your current key:
|
||||
```bash
|
||||
vantagectl verify /backups/vantage-backup-vantage-<date>.tar.gz \
|
||||
--mongo-uri mongodb://localhost:27017 --db vantage_drill
|
||||
```
|
||||
3. Drop the scratch database. It served its purpose.
|
||||
|
||||
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.
|
||||
|
||||
## When the key is wrong
|
||||
|
||||
If `restore` finds the archive's key fingerprint does not match the
|
||||
`KEY_ENCRYPTION_KEY` you are running with, it stops. Passing
|
||||
`--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
|
||||
|
||||
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 -
|
||||
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.
|
||||
@@ -24,7 +24,23 @@ values is permanently unreadable.
|
||||
Store the key somewhere other than the server it protects.
|
||||
:::
|
||||
|
||||
## Backing up MongoDB
|
||||
:::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
|
||||
**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
|
||||
`mongodump` does none of that: it records nothing about which key the data was
|
||||
encrypted under, so a restore from one succeeds even when the key is wrong and
|
||||
the failure only shows up later, as unreadable secrets.
|
||||
|
||||
The rest of this page, past the table above, describes the `mongodump` /
|
||||
`mongorestore` fallback for an operator who does not have `vantagectl`
|
||||
available. Prefer the linked page.
|
||||
:::
|
||||
|
||||
## Backing up MongoDB (fallback, without `vantagectl`)
|
||||
|
||||
With the bundled Mongo container:
|
||||
|
||||
@@ -33,6 +49,13 @@ docker compose exec -T mongo mongodump --archive --gzip --db vantage \
|
||||
> /backups/vantage-$(date +%F).archive.gz
|
||||
```
|
||||
|
||||
:::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
|
||||
to decrypt one.
|
||||
:::
|
||||
|
||||
Restoring:
|
||||
|
||||
```bash
|
||||
@@ -69,12 +92,13 @@ What it does **not** do is reconcile the world. After a restore:
|
||||
|
||||
| What | When |
|
||||
| ----------------- | ----------------------------------------------------- |
|
||||
| MongoDB dump | Nightly, retained per your policy |
|
||||
| Backup | Nightly, retained per your policy |
|
||||
| Environment file | On change, held in a password manager or secret store |
|
||||
| Restore rehearsal | Occasionally, into a throwaway host |
|
||||
|
||||
Rehearse a restore now and again. It is the step most often skipped, and the one
|
||||
that finds the problems.
|
||||
that finds the problems. See [Backup and restore](./backup-and-restore.md) for
|
||||
the drill, and for `verify`, which checks a backup is real without a restore.
|
||||
|
||||
## Cloud instances
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
| `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
|
||||
|
||||
@@ -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 | The web app | No, firewall it |
|
||||
| `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 |
|
||||
@@ -20,8 +20,8 @@ sidebar_label: Ports and networking
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B["Browser"] -->|HTTPS| P["Reverse proxy"]
|
||||
P --> W["web :3000"]
|
||||
W --> S["server :8080"]
|
||||
P -->|"everything else"| W["web :3000"]
|
||||
P -->|"/api /auth /public /install* /update*"| S["server :8080"]
|
||||
A["Agent on a managed server"] -->|"gRPC/TLS :9090, outbound"| S
|
||||
S --> G["guacd :4822"]
|
||||
G -->|"relayed over the :9090 stream"| A
|
||||
@@ -77,8 +77,20 @@ On a private network you can skip TLS instead, by setting `tls: false` in each
|
||||
|
||||
## Reverse proxy notes
|
||||
|
||||
- Point the proxy at `web:3000`. The web app reaches the API internally, so
|
||||
`8080` does not need publishing.
|
||||
- **The proxy routes two backends on one hostname**, and both are required:
|
||||
|
||||
| Path | Backend |
|
||||
| ------------------------------------------------------------- | ------------- |
|
||||
| `/api`, `/auth`, `/public`, `/install`, `/install.ps1`, `/update`, `/update.ps1` | `server:8080` |
|
||||
| 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` -
|
||||
including the login form.
|
||||
|
||||
- Both backends must be the **same** hostname and certificate. The browser
|
||||
calls `/api` relative to the page it is on, and the session cookie is
|
||||
host-only.
|
||||
- The console uses a **WebSocket** at `/api/console/tunnel`. A proxy that does
|
||||
not forward upgrade headers breaks the console and nothing else.
|
||||
- Workflow log streaming is a long-lived response. A short proxy read timeout
|
||||
|
||||
@@ -20,6 +20,13 @@ or is not 64 hex characters.
|
||||
|
||||
## Nobody can sign in
|
||||
|
||||
**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
|
||||
[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.
|
||||
|
||||
**`/setup` appears when users already exist.** The server is pointed at a
|
||||
different database than you think. Check the database name in `MONGO_URI`,
|
||||
which is taken from the end of the URI.
|
||||
@@ -117,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:
|
||||
|
||||
@@ -130,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
|
||||
@@ -163,14 +170,19 @@ 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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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**:
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -22,26 +22,41 @@ of lowercase letters, digits and `-`, starting and ending with a letter or
|
||||
digit. It becomes part of the public URL:
|
||||
|
||||
```
|
||||
https://<your-instance>.vantage.<yourdomain>/status/<page-id>
|
||||
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 -
|
||||
`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
|
||||
page address in the editor gives you the exact URL for your install, which is
|
||||
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
|
||||
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
|
||||
@@ -49,18 +64,21 @@ 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.
|
||||
|
||||
## What a visitor sees
|
||||
|
||||
- Component name, current state (up / down / under maintenance / unknown) and
|
||||
a 90-day uptime percentage.
|
||||
- Component name, current state (up / down / under maintenance / pending /
|
||||
unknown) and a 90-day uptime percentage. **Pending** is a monitor that has
|
||||
been added but has not produced a result yet; **unknown** is one nothing is
|
||||
checking any more.
|
||||
- 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 (info / warning / critical),
|
||||
for anything you want said regardless of component state.
|
||||
- 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
|
||||
are no severity levels to choose between.
|
||||
|
||||
A visitor never sees a target URL, host or port, the check's expected status
|
||||
or keyword, latency, a certificate expiry date, failure text, or which
|
||||
@@ -72,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
|
||||
@@ -93,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.
|
||||
@@ -102,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
+6
-1
@@ -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",
|
||||
],
|
||||
@@ -49,7 +54,7 @@ const sidebars: SidebarsConfig = {
|
||||
{
|
||||
type: "category",
|
||||
label: "Operations",
|
||||
items: ["operations/upgrading", "operations/backups", "operations/agent-updates"],
|
||||
items: ["operations/upgrading", "operations/backups", "operations/backup-and-restore", "operations/agent-updates"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+3
-3
@@ -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,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 |
Reference in New Issue
Block a user