feat: documentation site
Docusaurus 3 docs-only site at docsite/, served statically by nginx under /docs on the marketing host. Covers getting started (self-hosted install through first server and first licence), the control plane, Vantage HQ, a reference section and operations. Wired into docker-compose.site.yml as docsite (3005:80) and into the image build workflow, rebuilding on its own directory only. Never added to the self-hosted compose file.
This commit is contained in:
@@ -81,11 +81,13 @@ jobs:
|
||||
flag sitesvc '^(sitesvc/|shared/|go\.work)'
|
||||
flag admin '^(admin/|shared/|go\.work)'
|
||||
|
||||
# The three Next images use their own directory as the build
|
||||
# context, so nothing outside it can affect them.
|
||||
# The three Next images and the docs site use their own
|
||||
# directory as the build context, so nothing outside it can
|
||||
# affect them.
|
||||
flag web '^web/'
|
||||
flag site '^site/'
|
||||
flag adminsite '^adminsite/'
|
||||
flag docsite '^docsite/'
|
||||
|
||||
- name: Log in to registry
|
||||
run: |
|
||||
@@ -152,3 +154,18 @@ jobs:
|
||||
-t "$IMAGE" \
|
||||
-f adminsite/Dockerfile adminsite/
|
||||
docker push "$IMAGE"
|
||||
|
||||
- name: Build and push docsite image
|
||||
if: steps.changed.outputs.docsite == 'true'
|
||||
run: |
|
||||
IMAGE="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/docsite:latest"
|
||||
# DOCS_BASE_URL must match the proxy location that routes to
|
||||
# this container and the directory the image serves from.
|
||||
docker build \
|
||||
--build-arg DOCS_URL="${{ vars.DOCS_URL }}" \
|
||||
--build-arg DOCS_BASE_URL="${{ vars.DOCS_BASE_URL }}" \
|
||||
--build-arg APP_URL="${{ vars.APP_URL }}" \
|
||||
--build-arg HQ_URL="${{ vars.HQ_URL }}" \
|
||||
-t "$IMAGE" \
|
||||
-f docsite/Dockerfile docsite/
|
||||
docker push "$IMAGE"
|
||||
|
||||
@@ -55,4 +55,13 @@ services:
|
||||
- 3004:3000
|
||||
depends_on:
|
||||
- admin
|
||||
# Static docs, served by nginx at vantage.hostxtra.co.uk/docs through its own
|
||||
# proxy location. That location must sort ABOVE the catch-all forwarding to
|
||||
# site:3003, or Next serves its own 404 for /docs. The container serves from
|
||||
# /usr/share/nginx/html/docs because the proxy forwards the full path.
|
||||
docsite:
|
||||
image: gitea.hostxtra.co.uk/mrhid6/vantage/docsite:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 3005:80
|
||||
networks: {}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
build
|
||||
.docusaurus
|
||||
.git
|
||||
.gitignore
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
@@ -0,0 +1,40 @@
|
||||
# Build stage
|
||||
FROM node:26-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
# Baked in at build time. DOCS_BASE_URL must agree with three things at once:
|
||||
# the Nginx Proxy Manager location that routes to this container, the directory
|
||||
# the runtime stage serves from below, and this value. When they disagree the
|
||||
# HTML loads and every stylesheet and script 404s.
|
||||
ARG DOCS_URL="https://vantage.hostxtra.co.uk"
|
||||
ARG DOCS_BASE_URL="/docs/"
|
||||
ARG APP_URL="https://vantage.hostxtra.co.uk"
|
||||
ARG HQ_URL="https://vantage-hq.hostxtra.co.uk"
|
||||
ENV DOCS_URL=$DOCS_URL
|
||||
ENV DOCS_BASE_URL=$DOCS_BASE_URL
|
||||
ENV APP_URL=$APP_URL
|
||||
ENV HQ_URL=$HQ_URL
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Runtime stage
|
||||
#
|
||||
# Docusaurus emits a fully static site, so unlike web/, site/ and adminsite/
|
||||
# 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
|
||||
# terminates it.
|
||||
FROM nginx:alpine-slim AS runner
|
||||
|
||||
# NPM forwards the FULL request path upstream; it does not strip the /docs
|
||||
# prefix. Serving from a matching subdirectory means prefix, asset URLs and
|
||||
# upstream paths agree with no rewrite rule to keep in step.
|
||||
COPY --from=builder /app/build /usr/share/nginx/html/docs
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
id: claim-free-licence
|
||||
title: Claim a Free licence
|
||||
sidebar_label: Claim a Free licence
|
||||
---
|
||||
|
||||
A self-hosted install runs unlicensed until you give it a licence. Free is a
|
||||
real tier in both deployments, and you can claim one for your install from the
|
||||
HQ portal.
|
||||
|
||||
## What a licence is
|
||||
|
||||
A signed file. It carries the instance UUID it belongs to, the tier, the server
|
||||
allowance, feature toggles and an expiry. The control plane verifies the
|
||||
signature locally — checking a licence never contacts HQ, and a running instance
|
||||
does not need HQ to be reachable.
|
||||
|
||||
Signing happens in exactly one place, in HQ. The control plane can only verify.
|
||||
|
||||
## 1. Find your instance UUID
|
||||
|
||||
In the control plane, go to **Settings → Licence**. The instance UUID is shown
|
||||
there. It is the identity your licence binds to.
|
||||
|
||||
## 2. Link the install to your HQ account
|
||||
|
||||
1. Sign in at [Vantage HQ](https://vantage-hq.hostxtra.co.uk). If you have no
|
||||
account, see [Accounts and signup](../hq/accounts-and-signup.md).
|
||||
2. Choose **Link an instance**.
|
||||
3. Paste the instance UUID and give it a name you will recognise.
|
||||
|
||||
Linking claims the UUID for your account. A UUID already linked elsewhere is
|
||||
refused with a conflict rather than silently moved.
|
||||
|
||||
## 3. Claim Free
|
||||
|
||||
With the instance linked, choose **Claim Free** on it. HQ issues a Free licence
|
||||
bound to that UUID and hands it back.
|
||||
|
||||
:::info One Free per account, per deployment
|
||||
The limit is enforced per account **and** deployment, so a Free cloud instance
|
||||
does not stop you claiming Free on a self-hosted install. Both the friendly
|
||||
pre-check and the issuer apply the same rule — deliberately, because a
|
||||
pre-check stricter than the issuer would refuse something that would actually
|
||||
have worked.
|
||||
:::
|
||||
|
||||
## 4. Install the licence
|
||||
|
||||
Download the licence from HQ and paste it in the control plane at
|
||||
**Settings → Licence**.
|
||||
|
||||
The instance validates the signature, checks the UUID matches its own, and
|
||||
starts reporting the tier, allowance and expiry.
|
||||
|
||||
:::warning Cloud instances cannot paste a licence
|
||||
On a cloud instance `POST /license` answers `409 cloud_managed`, and the UI
|
||||
hides the form entirely. A cloud licence is written directly by HQ. This is not
|
||||
a restriction the injection path has to work around — it writes to the database,
|
||||
not through the endpoint.
|
||||
:::
|
||||
|
||||
## Renewing
|
||||
|
||||
Free licences are renewable from HQ within a renewal window near expiry;
|
||||
outside that window the renew call refuses. See [Free tier](../hq/free-tier.md).
|
||||
|
||||
Pasting a licence keeps working while the current one is expired — that endpoint
|
||||
is exempt from the licence check, because it is the way out of degraded mode.
|
||||
|
||||
## Moving the install to new hardware
|
||||
|
||||
Rebuilding produces a new instance UUID, and a licence binds to a UUID. Use
|
||||
**Relink** in HQ to move the licence across. The number of relinks per term is
|
||||
capped; the portal shows how many you have left.
|
||||
|
||||
## Next
|
||||
|
||||
- [Licensing and entitlements](../hq/licensing-and-entitlements.md)
|
||||
- [Buying a paid self-hosted licence](../hq/self-hosted-instances.md)
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
id: cloud-vs-self-hosted
|
||||
title: Cloud or self-hosted
|
||||
sidebar_label: Cloud or self-hosted
|
||||
---
|
||||
|
||||
Vantage runs in two deployments. They are the same software; what differs is
|
||||
who operates it and how licensing, users and data lifecycle work.
|
||||
|
||||
## At a glance
|
||||
|
||||
| | Cloud | Self-hosted |
|
||||
| --- | --- | --- |
|
||||
| Who runs it | We do | You do |
|
||||
| Where you sign in | `<your-slug>.vantage.hostxtra.co.uk` | Your own hostname |
|
||||
| Database and backups | Ours | Yours |
|
||||
| Licence | Written for you when you buy or create the instance | Pasted in, or claimed from HQ |
|
||||
| Team members | Granted from HQ; the instance holds a projection | Created in the instance itself |
|
||||
| Free tier | Yes, one per account | Yes, one per account |
|
||||
| Expired Free instance | Eventually deleted, after warning | Never deleted |
|
||||
|
||||
## Cloud
|
||||
|
||||
You create an instance from the HQ portal and it exists a few seconds later,
|
||||
already licensed. People you grant access to get a real user inside that
|
||||
instance — see [People and roles](../hq/people-and-roles.md) — but HQ owns their
|
||||
password, role and existence.
|
||||
|
||||
:::info The instance does not phone home
|
||||
A grant writes a user row into the control plane once. After that the instance
|
||||
authenticates that person entirely on its own. HQ being down does not stop
|
||||
anyone signing in to a running instance.
|
||||
:::
|
||||
|
||||
Cloud instances on the Free tier are reaped after their licence expires, with
|
||||
warning emails first. See [Free tier](../hq/free-tier.md).
|
||||
|
||||
## Self-hosted
|
||||
|
||||
You run the Docker Compose stack on your own infrastructure. Nothing about the
|
||||
control plane requires an internet connection to HQ at runtime — a licence is a
|
||||
signed file, verified locally.
|
||||
|
||||
Two ways to get one:
|
||||
|
||||
1. **Free** — link the install to an HQ account and claim it
|
||||
([Claim a Free licence](./claim-free-licence.md)).
|
||||
2. **Paid** — buy from HQ, which creates a placeholder, then paste the install's
|
||||
real instance UUID to bind and issue
|
||||
([Self-hosted instances](../hq/self-hosted-instances.md)).
|
||||
|
||||
Self-hosted users are local (or OIDC). There is no projection from HQ, and the
|
||||
three member endpoints in HQ refuse to touch a self-hosted instance at all.
|
||||
|
||||
:::warning Self-hosted instances are never deleted by us
|
||||
The reaper that removes expired Free cloud instances is disabled by default and
|
||||
must stay that way on a self-hosted install. See `FREE_INSTANCE_REAP_AFTER` in
|
||||
[Environment variables](../reference/environment-variables.md).
|
||||
:::
|
||||
|
||||
## Which should you pick
|
||||
|
||||
Pick cloud if you want the thing running now and do not want to own a MongoDB.
|
||||
Pick self-hosted if your policy requires the control plane inside your own
|
||||
network, or the servers you manage cannot reach the public internet.
|
||||
|
||||
Moving between them is a migration, not a switch — instances are bound to a
|
||||
deployment at creation, and a licence binds to an instance UUID.
|
||||
|
||||
## Next
|
||||
|
||||
- [Self-hosted install](./self-hosted-install.md)
|
||||
- [Accounts and signup](../hq/accounts-and-signup.md) if you are going cloud
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
id: first-login
|
||||
title: First login
|
||||
sidebar_label: First login
|
||||
---
|
||||
|
||||
A fresh install has no users and no organisation. The first visit creates both.
|
||||
|
||||
## 1. Bootstrap
|
||||
|
||||
Open the control plane in a browser. Because no user exists, you land on
|
||||
`/setup`.
|
||||
|
||||
Fill in:
|
||||
|
||||
| Field | Notes |
|
||||
| --- | --- |
|
||||
| Organisation name | Display name. Shown throughout the UI |
|
||||
| Slug | Lowercase, used in the hostname on cloud. Some names are reserved |
|
||||
| Your name | |
|
||||
| Email | Becomes your sign-in identity |
|
||||
| Password | Stored bcrypt-hashed |
|
||||
|
||||
Submitting creates the organisation and its **owner** — you.
|
||||
|
||||
:::warning Bootstrap works exactly once
|
||||
The endpoint is open only while the database has no users. As soon as the first
|
||||
one exists, `/setup` redirects to the login page and the bootstrap endpoint
|
||||
refuses. There is no second chance to create the first owner, so record the
|
||||
credentials before you close the tab.
|
||||
:::
|
||||
|
||||
## 2. Sign in
|
||||
|
||||
You are taken to `/login`. Sign in with the email and password you just set.
|
||||
|
||||
Sessions are an opaque 32-byte token in the `km_session` cookie, with the body
|
||||
held in Redis for 24 hours. Restarting Redis signs everyone out and loses
|
||||
nothing else.
|
||||
|
||||
## 3. Look around
|
||||
|
||||
You land on the fleet dashboard, which is empty. The sidebar is the whole
|
||||
product:
|
||||
|
||||
| Section | What it does |
|
||||
| --- | --- |
|
||||
| Servers | The fleet — enrol, inspect, console, update |
|
||||
| Keys | SSH public keys and their assignments |
|
||||
| Workflows | Compose and run scripted work |
|
||||
| Steps | The reusable step library |
|
||||
| Monitors | HTTP, TCP, ICMP and TLS checks |
|
||||
| Secrets | The encrypted vault |
|
||||
| Audit | Every mutating action |
|
||||
| Settings | Members, SSO, alerts, retention, licence |
|
||||
|
||||
## 4. Add the rest of your team
|
||||
|
||||
Go to **Settings → Access**. Add members with a role:
|
||||
|
||||
| Role | Can |
|
||||
| --- | --- |
|
||||
| `owner` | Everything, including billing-adjacent settings |
|
||||
| `admin` | Everything except owner-only settings |
|
||||
| `member` | Day-to-day work — servers, keys, workflows, monitors |
|
||||
|
||||
Settings and organisation management require `owner` or `admin`.
|
||||
|
||||
If you would rather not manage passwords, configure OIDC instead — see
|
||||
[Settings](../vantage/settings.md#single-sign-on-oidc). OIDC is configured per
|
||||
organisation, and the client secret is stored encrypted.
|
||||
|
||||
## Next
|
||||
|
||||
[Add your first server](./first-server.md).
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
id: first-server
|
||||
title: Add your first server
|
||||
sidebar_label: Add your first server
|
||||
---
|
||||
|
||||
Enrolling a machine means running one command on it. The control plane issues a
|
||||
short-lived token, the install script fetches the agent and writes a config, and
|
||||
the machine registers itself.
|
||||
|
||||
## 1. Create the enrolment
|
||||
|
||||
In the UI, go to **Servers → Add server**. That calls `POST /api/servers/new`,
|
||||
which generates a server ID and a pre-registration token and hands back a ready
|
||||
one-liner.
|
||||
|
||||
:::warning The token is single-use and lives one hour
|
||||
It is the only credential in the flow, and it is spent the moment the agent
|
||||
calls `Register`. If you paste it somewhere and come back tomorrow, create a new
|
||||
enrolment instead — nothing is lost by doing so.
|
||||
:::
|
||||
|
||||
## 2. Run the one-liner
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
curl -fsSL "https://vantage.example.com/install?server_id=<id>&token=<token>" | bash
|
||||
```
|
||||
|
||||
Run it as root. The script:
|
||||
|
||||
1. Detects architecture — `x86_64` and `aarch64` only; anything else exits.
|
||||
2. Asks the Gitea API for the newest `agent/v*` release.
|
||||
3. Downloads the binary and `checksums.txt`, and **verifies the SHA-256**,
|
||||
aborting on a mismatch.
|
||||
4. Installs to `/usr/local/bin/vantage-agent`, mode `0755`.
|
||||
5. Writes `/etc/vantage/config.yaml` (directory `0700`, file `0600`) containing
|
||||
the server ID, the pre-registration token and the gRPC host.
|
||||
6. Writes `/etc/systemd/system/vantage-agent.service` with `Restart=always`, and
|
||||
runs `systemctl enable --now vantage-agent`.
|
||||
|
||||
### Windows
|
||||
|
||||
```powershell
|
||||
irm "https://vantage.example.com/install.ps1?server_id=<id>&token=<token>" | iex
|
||||
```
|
||||
|
||||
Run from an elevated PowerShell. The agent is registered as a service through
|
||||
NSSM, with the config at `%ProgramData%\vantage\config.yaml`. There is also an
|
||||
MSI built by CI if you would rather deploy that.
|
||||
|
||||
:::info Windows agents are second-class on purpose
|
||||
They register, heartbeat, run workflow steps and report inventory. They do
|
||||
**not** manage `authorized_keys` — the key subsystem is Linux-only, and a
|
||||
Windows agent stops after the heartbeat portion of the poll.
|
||||
:::
|
||||
|
||||
## 3. Watch it come up
|
||||
|
||||
The server appears immediately as `pending`. Within one poll interval — 30
|
||||
seconds — it flips to `active`.
|
||||
|
||||
On the machine:
|
||||
|
||||
```bash
|
||||
systemctl status vantage-agent
|
||||
journalctl -u vantage-agent -f
|
||||
```
|
||||
|
||||
What happens on that first run:
|
||||
|
||||
```
|
||||
1. Load /etc/vantage/config.yaml
|
||||
2. pre_reg_token present → Register() → save agent_token, clear pre_reg_token
|
||||
3. Reconnect with the permanent token
|
||||
4. Start: command stream · hourly update check · inventory · monitors
|
||||
5. Enter the SyncKeys poll loop
|
||||
```
|
||||
|
||||
After registration the config no longer contains the pre-registration token; it
|
||||
contains a permanent agent token instead. The control plane stores only the
|
||||
SHA-256 of that token, never the token itself.
|
||||
|
||||
## 4. Confirm it works
|
||||
|
||||
Open the server's detail page. Within a minute or two you should see:
|
||||
|
||||
- Status `active`, with a recent last-seen timestamp.
|
||||
- Inventory — CPU, memory, swap, partitions, kernel. Metrics refresh every 30
|
||||
seconds; the full static snapshot every 15 minutes.
|
||||
- Pending OS updates, checked hourly.
|
||||
|
||||
## If it does not appear
|
||||
|
||||
| Symptom | Cause |
|
||||
| --- | --- |
|
||||
| Script exits at "Unsupported architecture" | Not amd64 or arm64 |
|
||||
| "Checksum mismatch!" | Interrupted download, or a proxy rewriting the body. Re-run |
|
||||
| "Could not determine latest agent version" | The host cannot reach `gitea.hostxtra.co.uk`, or no `agent/v*` release exists |
|
||||
| Service runs, server stays `pending` | The machine cannot reach `GRPC_HOST`. Test it from that machine |
|
||||
| Registers once then goes `offline` | Reachable for `Register` but not for the poll — usually a firewall that permits the initial connection but drops the long-lived one |
|
||||
|
||||
A server is marked `offline` when its last-seen time passes the threshold; that
|
||||
sweep runs every two minutes, so allow for it before concluding anything.
|
||||
|
||||
## Next
|
||||
|
||||
- [Assign an SSH key](../vantage/ssh-keys.md)
|
||||
- [Run a workflow](../vantage/workflows.md)
|
||||
- [Claim a Free licence](./claim-free-licence.md)
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
id: self-hosted-install
|
||||
title: Install Vantage (self-hosted)
|
||||
sidebar_label: Self-hosted install
|
||||
---
|
||||
|
||||
This installs the control plane on a host you own. Budget about fifteen minutes.
|
||||
|
||||
## Before you start
|
||||
|
||||
You need:
|
||||
|
||||
- A Linux host with **Docker** and the **Compose plugin**.
|
||||
- A DNS name pointing at it. You will use it for both the web UI and, with a
|
||||
port, for agents.
|
||||
- A reverse proxy terminating TLS in front of the web UI. gRPC on `:9090` is
|
||||
reached directly by agents.
|
||||
- Two ports reachable from every machine you intend to manage: the web port for
|
||||
people, and **9090** for agents.
|
||||
- Outbound access from the control plane, and from every managed machine, to
|
||||
`gitea.hostxtra.co.uk`, which serves the agent releases.
|
||||
|
||||
The stack itself brings MongoDB, Redis and guacd with it. You do not need to
|
||||
provide a database.
|
||||
|
||||
## 1. Get the compose file
|
||||
|
||||
Put `deploy/docker-compose.yml` from the repository in a working directory, for
|
||||
example `/opt/vantage`.
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/vantage/data && cd /opt/vantage
|
||||
# copy docker-compose.yml here
|
||||
```
|
||||
|
||||
The `server` service bind-mounts `./data`, which is where workflow run logs are
|
||||
written. Create it before first boot so it is not owned by root-in-container in
|
||||
a way you did not intend.
|
||||
|
||||
## 2. Write the environment file
|
||||
|
||||
Create `/opt/vantage/.env`:
|
||||
|
||||
```bash
|
||||
# The host:port agents dial. NOT the web URL — this port speaks gRPC.
|
||||
GRPC_HOST=vantage.example.com:9090
|
||||
|
||||
|
||||
# 32 bytes as 64 hex characters. Generate with the command below.
|
||||
KEY_ENCRYPTION_KEY=
|
||||
|
||||
# Optional: where workflow run logs are written inside the container.
|
||||
VANTAGE_WORKFLOW_LOG_DIR=/data/workflow-logs
|
||||
```
|
||||
|
||||
Generate the encryption key:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
:::danger Keep the encryption key
|
||||
`KEY_ENCRYPTION_KEY` encrypts SSH private keys, vault secrets, OIDC client
|
||||
secrets and console credentials with AES-256-GCM. Lose it and every one of those
|
||||
becomes unreadable — there is no recovery path. Back it up somewhere other than
|
||||
the server it protects, and never rotate it without a planned re-encryption.
|
||||
:::
|
||||
|
||||
:::warning `GRPC_HOST` has no default
|
||||
The server refuses to boot without it. There is deliberately no fallback to the
|
||||
web host: that would hand every agent a port that does not speak gRPC, and the
|
||||
failure would only surface later, on each agent, as a connection error.
|
||||
:::
|
||||
|
||||
## 3. Start the stack
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
Five services come up: `mongo`, `redis`, `guacd`, `server` and `web`.
|
||||
|
||||
Check the server got through boot:
|
||||
|
||||
```bash
|
||||
docker compose logs -f server
|
||||
```
|
||||
|
||||
Boot runs database migrations, builds indexes and seeds the default workflow
|
||||
step library. Index builders for auth and settings are **fatal on failure** —
|
||||
they enforce tenant isolation, so the server would rather not start than start
|
||||
without them.
|
||||
|
||||
## 4. Put a proxy in front
|
||||
|
||||
Point your reverse proxy at `web` on port `3000` and terminate TLS there. The
|
||||
web app calls the REST API through a Next rewrite, so you do not need to expose
|
||||
`8080` publicly.
|
||||
|
||||
Do **not** proxy `9090`. Agents connect to it directly over TLS.
|
||||
|
||||
## 5. First sign-in
|
||||
|
||||
Open your hostname in a browser. With no users in the database, you are sent to
|
||||
`/setup`.
|
||||
|
||||
Continue with [First login](./first-login.md).
|
||||
|
||||
## Verifying the install
|
||||
|
||||
| Check | Expected |
|
||||
| --- | --- |
|
||||
| `docker compose ps` | five services `running` |
|
||||
| `curl -s localhost:8080/auth/bootstrap-status` | JSON saying bootstrap is needed |
|
||||
| `nc -z your-host 9090` | open |
|
||||
| `docker compose logs server \| grep -i fatal` | nothing |
|
||||
|
||||
## Common install problems
|
||||
|
||||
**Server exits immediately.** Almost always a missing `GRPC_HOST`. The log line
|
||||
names it.
|
||||
|
||||
**Agents register but never go active.** They reached `:9090` for `Register` but
|
||||
cannot sustain the poll, or `GRPC_HOST` names a host they resolve differently.
|
||||
Check from the managed machine, not from the control plane host.
|
||||
|
||||
**Secrets pages error.** `KEY_ENCRYPTION_KEY` is empty or not 64 hex characters.
|
||||
|
||||
More in [Troubleshooting](../reference/troubleshooting.md).
|
||||
|
||||
## What this install does not include
|
||||
|
||||
The marketing site, the public form service, the HQ portal and this
|
||||
documentation site are separate services in `deploy/docker-compose.site.yml`.
|
||||
A self-hosted install deliberately runs none of them, and in particular never
|
||||
holds the licence signing key.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
id: what-is-vantage
|
||||
title: What is Vantage
|
||||
sidebar_label: What is Vantage
|
||||
---
|
||||
|
||||
Vantage manages a fleet of servers from one place. It began as SSH key
|
||||
management and grew outwards: key assignment, scripted workflow execution,
|
||||
service monitoring, a secrets vault, a browser-based console and OS update
|
||||
management.
|
||||
|
||||
## The pieces
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
W["Web UI<br/>servers · keys · workflows · monitors<br/>secrets · audit · console · settings"]
|
||||
S["Server<br/>REST :8080 · gRPC :9090<br/>MongoDB · Redis · guacd"]
|
||||
A["Agent<br/>one per managed server<br/>Linux and Windows"]
|
||||
W -->|REST, cookie session| S
|
||||
S -->|gRPC over TLS| A
|
||||
A -.->|outbound only| S
|
||||
```
|
||||
|
||||
**The server** holds all state and does all decision-making. It exposes a REST
|
||||
API on `:8080` for the web UI and a gRPC API on `:9090` for agents. MongoDB
|
||||
stores everything durable; Redis stores sessions and nothing else.
|
||||
|
||||
**The agent** is a single Go binary running as root on each managed server. It
|
||||
polls the control plane every 30 seconds for desired key state, and holds a
|
||||
bidirectional command stream so the server can push work — run a workflow step,
|
||||
generate a key, apply updates — without waiting for the next poll.
|
||||
|
||||
**The web UI** is the operator interface. Everything it does goes through the
|
||||
REST API, which is the actual security boundary; the UI only ever makes things
|
||||
convenient.
|
||||
|
||||
## How agents connect
|
||||
|
||||
The agent dials **out** to the control plane. There is no inbound listener on a
|
||||
managed server, no port to open and no NAT traversal to arrange. If the machine
|
||||
can reach your Vantage host on the gRPC port, it can be managed.
|
||||
|
||||
That direction is why `GRPC_HOST` exists as an explicit setting: the agent has
|
||||
to be told a `host:port` it can reach, and there is no safe default the server
|
||||
could guess on its behalf.
|
||||
|
||||
## Two request patterns
|
||||
|
||||
| Pattern | Used for | Why |
|
||||
| --- | --- | --- |
|
||||
| Poll (`SyncKeys`, every 30s) | desired SSH key state | Key changes are not urgent, and polling survives a dropped connection with no reconnection logic |
|
||||
| Push (`CommandStream`) | workflow steps, key generation, updates, agent self-update | Clicking Run should not wait up to 30 seconds |
|
||||
|
||||
## Multi-tenancy
|
||||
|
||||
Every document in the database carries an instance ID, and every query is scoped
|
||||
by it. One deployment can therefore host many independent tenants. On a
|
||||
self-hosted install that mechanism is still there — you simply have one tenant.
|
||||
|
||||
## Next
|
||||
|
||||
- [Cloud or self-hosted](./cloud-vs-self-hosted.md) — which one you want
|
||||
- [Self-hosted install](./self-hosted-install.md) — stand it up
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
id: accounts-and-signup
|
||||
title: Accounts and signup
|
||||
sidebar_label: Accounts and signup
|
||||
---
|
||||
|
||||
Vantage HQ, at `vantage-hq.hostxtra.co.uk`, is where you manage the **account**
|
||||
behind your instances: your team, your instances, their licences and billing.
|
||||
|
||||
## An account is a team, not a person
|
||||
|
||||
One account holds many people and many instances. Everyone in it has an account
|
||||
role:
|
||||
|
||||
| Role | Can |
|
||||
| --- | --- |
|
||||
| `owner` | Everything, including billing |
|
||||
| `admin` | Invite people, create instances, grant instance access |
|
||||
| `member` | Read what the account holds |
|
||||
|
||||
Reading is open to any signed-in member. Every mutation except changing your own
|
||||
password requires `owner` or `admin`. Billing is owner-only.
|
||||
|
||||
The three words are the same as the control plane's roles, on purpose — but they
|
||||
are separate things. Your account role governs the portal; your role *inside* an
|
||||
instance governs that instance.
|
||||
|
||||
## Signing up
|
||||
|
||||
Signup is **account-first**. Creating an account creates the account and you;
|
||||
it does not create a Vantage instance. Nothing exists in any control plane until
|
||||
you later create or link one.
|
||||
|
||||
1. Go to the signup form.
|
||||
2. Enter your name, email and a password.
|
||||
3. Check your email and click the verification link.
|
||||
|
||||
:::info Verify before you can sign in
|
||||
An unverified account gets a distinct "check your email" message rather than a
|
||||
generic authentication failure — the address is already known to be yours, so
|
||||
there is nothing to protect by being vague.
|
||||
:::
|
||||
|
||||
Verification links are valid for **24 hours**. The token is 32 random bytes and
|
||||
only its SHA-256 hash is stored, so a leaked database yields no working links.
|
||||
|
||||
If the verification email cannot be sent, the signup is rolled back rather than
|
||||
left stranded — retry rather than assuming a half-created account is in the way.
|
||||
|
||||
## Signing in
|
||||
|
||||
Email and password. The session is a cookie, separate from the control plane's:
|
||||
signing in to HQ does not sign you in to an instance, and vice versa.
|
||||
|
||||
## What comes next
|
||||
|
||||
| You want | Go to |
|
||||
| --- | --- |
|
||||
| A Vantage instance we run | [Cloud instances](./cloud-instances.md) |
|
||||
| To license an install you run | [Self-hosted instances](./self-hosted-instances.md) |
|
||||
| To add colleagues | [People and roles](./people-and-roles.md) |
|
||||
| To understand tiers and limits | [Licensing and entitlements](./licensing-and-entitlements.md) |
|
||||
|
||||
## The portal layout
|
||||
|
||||
Three destinations: **Overview**, **People**, **Billing**.
|
||||
|
||||
Settings lives in the account menu rather than the nav, because it is your
|
||||
password rather than a place. The appearance toggle is there too.
|
||||
|
||||
Overview lists your instances. Each is one record, closed to a row and open to
|
||||
its licence contents, members and actions. It opens by default when it is your
|
||||
only instance or when it needs attention, and your manual choice is remembered.
|
||||
|
||||
There is deliberately no "your plan" card in the sidebar: tier, limits and
|
||||
expiry belong to a **licence**, and a licence belongs to one instance. An
|
||||
account with a Free cloud instance and a Professional self-hosted one has no
|
||||
single plan to show.
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
id: billing
|
||||
title: Billing
|
||||
sidebar_label: Billing
|
||||
---
|
||||
|
||||
Paid plans are billed through **Paddle**, which is the merchant of record. Your
|
||||
invoice, your card details and your tax handling are all Paddle's; HQ holds a
|
||||
customer reference and nothing sensitive.
|
||||
|
||||
Billing is **owner-only**.
|
||||
|
||||
## Buying
|
||||
|
||||
### Cloud
|
||||
|
||||
Open the instance, change its configuration to what you want, and check out.
|
||||
Checkout runs in the browser.
|
||||
|
||||
### Self-hosted
|
||||
|
||||
**Buy self-hosted**, then bind the purchase to your install's UUID. See
|
||||
[Self-hosted instances](./self-hosted-instances.md).
|
||||
|
||||
## What you are buying
|
||||
|
||||
A subscription's line items are the configuration: the plan base, the metered
|
||||
server count above the base, and any per-instance features. Changing the
|
||||
configuration changes the line items.
|
||||
|
||||
## Changing configuration
|
||||
|
||||
**Instance → Configuration**, adjust servers or features, and save.
|
||||
|
||||
- **Increases** take effect when the payment confirms.
|
||||
- **Reductions** are scheduled for the end of the term. The portal shows the
|
||||
date and the new value.
|
||||
|
||||
## The customer portal
|
||||
|
||||
**Billing → Manage** mints a Paddle customer-portal session where you can
|
||||
update your payment method, see invoices and cancel.
|
||||
|
||||
## How a licence follows a payment
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C["Checkout / change"] --> P["Paddle"]
|
||||
P -->|signed webhook| H["HQ"]
|
||||
H --> G["Entitlement: desired → granted"]
|
||||
G --> L["Licence signed from granted"]
|
||||
```
|
||||
|
||||
The webhook is the **only** issuing path for paid plans. It is signature
|
||||
verified, processed exactly once, and resolved from the subscription's *current*
|
||||
line items — so a webhook that arrives out of order still produces the right
|
||||
answer rather than replaying a stale state.
|
||||
|
||||
A licence is signed from **granted** only. A checkout you abandon changes
|
||||
nothing.
|
||||
|
||||
## Cancelling and failed payments
|
||||
|
||||
Cancelling, or a payment going past due, takes **no immediate licence action**.
|
||||
Your licence runs to its grace-padded expiry and then lapses normally. There is
|
||||
no mid-term cut-off.
|
||||
|
||||
For a cloud Free instance, lapsing eventually leads to deletion — see
|
||||
[Free tier](./free-tier.md). Paid instances are not reaped.
|
||||
|
||||
## Renewals
|
||||
|
||||
At renewal the subscription bills again and the licence is reissued for the new
|
||||
term. It is also the only moment a scheduled **reduction** takes effect.
|
||||
|
||||
Self-hosted customers: download and paste the reissued licence. Cloud customers:
|
||||
nothing to do.
|
||||
|
||||
## Free is not in Paddle at all
|
||||
|
||||
Free has no subscription, no £0 line item and no Paddle record. It has its own
|
||||
renewal, in the portal. An account only acquires a Paddle customer reference
|
||||
with its first paid purchase.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
id: cloud-instances
|
||||
title: Cloud instances
|
||||
sidebar_label: Cloud instances
|
||||
---
|
||||
|
||||
A cloud instance is a Vantage control plane we run for you, reachable at
|
||||
`<your-slug>.vantage.hostxtra.co.uk`.
|
||||
|
||||
## Creating one
|
||||
|
||||
1. **Overview → New instance**.
|
||||
2. Choose a name and a slug.
|
||||
3. Create.
|
||||
|
||||
The instance is provisioned with you as its owner, and a Free licence is issued
|
||||
immediately. The owner user inside it gets your HQ password hash **copied**, not
|
||||
shared — see [People and roles](./people-and-roles.md).
|
||||
|
||||
### Slugs
|
||||
|
||||
The slug becomes your hostname label, so it is lowercase, and some names are
|
||||
reserved. Pick something you can say on a phone call.
|
||||
|
||||
:::warning One Free instance per account, per deployment
|
||||
Creating a second Free cloud instance is refused. If you want another, it needs
|
||||
a paid plan — or free up the first.
|
||||
:::
|
||||
|
||||
## Using it
|
||||
|
||||
Sign in at your instance's hostname with the email and password you use for HQ.
|
||||
It is a normal Vantage install from that point: see
|
||||
[Getting started](../getting-started/first-login.md) and the
|
||||
[Vantage](../vantage/servers.md) section.
|
||||
|
||||
The instance does not depend on HQ at runtime. HQ being unreachable does not
|
||||
affect anyone signing in or any agent syncing.
|
||||
|
||||
## The instance record
|
||||
|
||||
Each instance on Overview is one record. Closed, it is a row. Open, it shows:
|
||||
|
||||
- **Licence contents** — tier, server allowance, features, expiry.
|
||||
- **Members** — who has access and with what instance role.
|
||||
- **Actions** — grant access, change configuration, renew.
|
||||
|
||||
## Members
|
||||
|
||||
Granting access writes a real user into the instance. Covered fully in
|
||||
[People and roles](./people-and-roles.md).
|
||||
|
||||
## Changing what it can do
|
||||
|
||||
Server allowance and per-instance features (browser console, single sign-on) are
|
||||
part of the instance's **entitlement**. Changing it goes through billing — see
|
||||
[Licensing and entitlements](./licensing-and-entitlements.md) and
|
||||
[Billing](./billing.md).
|
||||
|
||||
## Renaming
|
||||
|
||||
The display name is free to change. The slug is the hostname and is not
|
||||
casually changed — ask support if you need it.
|
||||
|
||||
## What happens if the licence lapses
|
||||
|
||||
A cloud instance whose Free licence expires enters degraded mode, then, after a
|
||||
grace period, the instance and all its data are deleted. Warning emails go out
|
||||
first. See [Free tier](./free-tier.md).
|
||||
|
||||
Paid instances do not get reaped by that mechanism. A cancelled subscription
|
||||
runs to its grace-padded expiry and then lapses.
|
||||
|
||||
## Deleting
|
||||
|
||||
Ask support. Deletion is performed by the control plane, not by HQ — the control
|
||||
plane is the only service that knows which collections carry the instance ID,
|
||||
and duplicating that list into HQ would be a list that drifts.
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
id: free-tier
|
||||
title: Free tier
|
||||
sidebar_label: Free tier
|
||||
---
|
||||
|
||||
Free is a real tier in both deployments — not a trial that turns into nothing.
|
||||
|
||||
## What you get
|
||||
|
||||
| | Free |
|
||||
| --- | --- |
|
||||
| Servers | 3 |
|
||||
| Monitors | 3 |
|
||||
| Secret groups | 1 |
|
||||
| Notification channels | 1 |
|
||||
| Audit retention | 30 days |
|
||||
| Support | Community |
|
||||
|
||||
Browser console and single sign-on are not included; they are per-instance
|
||||
features on a paid plan.
|
||||
|
||||
## One per account, per deployment
|
||||
|
||||
The limit is enforced per account **and** deployment. A Free cloud instance does
|
||||
not prevent a Free self-hosted one — they are separate slots.
|
||||
|
||||
## Free is outside Paddle
|
||||
|
||||
There is no subscription, no £0 line item and no invoice. Your account acquires
|
||||
a Paddle customer reference only with its first paid purchase.
|
||||
|
||||
## Renewing
|
||||
|
||||
Free licences have a term and must be renewed from the portal.
|
||||
|
||||
- The renew button appears **7 days before expiry**.
|
||||
- It stays available **after** expiry, right up until the instance is reaped —
|
||||
so the same button rescues a lapsed instance rather than needing a second
|
||||
mechanism.
|
||||
- Renewing outside that window is refused, and the message names the date it
|
||||
opens.
|
||||
|
||||
:::tip Put the expiry in a calendar
|
||||
Warning emails go to the account address. If nobody watches that inbox, a Free
|
||||
cloud instance can lapse and eventually be deleted without anyone noticing.
|
||||
:::
|
||||
|
||||
## What happens when it lapses
|
||||
|
||||
**Self-hosted:** the instance goes into degraded mode after the grace period and
|
||||
stays that way. Nothing is deleted, ever — the reaper is disabled by default on
|
||||
a self-hosted install and must stay that way.
|
||||
|
||||
**Cloud:** the instance goes into degraded mode, and after a further period the
|
||||
instance **and all its data are deleted**. Warning emails are sent first, naming
|
||||
the date.
|
||||
|
||||
:::danger Deletion is permanent
|
||||
There is no restore. If a cloud Free instance is approaching that date and you
|
||||
want to keep it, renew it, or move it to a paid plan.
|
||||
:::
|
||||
|
||||
Deletion is carried out by the control plane rather than by HQ. HQ sends the
|
||||
warnings because it knows the billing address; the control plane performs the
|
||||
delete because it is the only service that knows which collections carry the
|
||||
instance ID.
|
||||
|
||||
## Moving off Free
|
||||
|
||||
Change the instance's configuration to a paid tier and check out. Your data
|
||||
stays where it is — a tier change reissues a licence, it does not rebuild
|
||||
anything.
|
||||
|
||||
## Relinks
|
||||
|
||||
Free instances get the same relink allowance as paid ones: three per term. That
|
||||
cap exists to put a human in front of a fourth attempt, not to obstruct a
|
||||
genuine rebuild.
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
id: licensing-and-entitlements
|
||||
title: Licensing and entitlements
|
||||
sidebar_label: Licensing and entitlements
|
||||
---
|
||||
|
||||
A **licence** is a signed statement of what one instance may do. An
|
||||
**entitlement** is the configuration a licence is cut from.
|
||||
|
||||
## Tiers
|
||||
|
||||
Three tiers, in both deployments. The allowances are identical across cloud and
|
||||
self-hosted — what differs is the term on offer, not what you get.
|
||||
|
||||
| | Free | Professional | Enterprise |
|
||||
| --- | --- | --- | --- |
|
||||
| Servers (base) | 3 | 3 | 10 |
|
||||
| Monitors | 3 | unlimited | unlimited |
|
||||
| Secret groups | 1 | unlimited | unlimited |
|
||||
| Notification channels | 1 | unlimited | unlimited |
|
||||
| Audit retention | 30 days | 365 days | unlimited |
|
||||
| Support | Community | Email, 24×5 | Email and phone, 24×7 |
|
||||
|
||||
The server count is **metered**: the base allowance comes with the tier, and you
|
||||
buy additional servers on top. That is why Professional shows a real number
|
||||
rather than "unlimited" — the number you actually have is the one in your
|
||||
entitlement.
|
||||
|
||||
## Features
|
||||
|
||||
Two are per-instance toggles rather than tier bundles:
|
||||
|
||||
| Feature | What it enables |
|
||||
| --- | --- |
|
||||
| `console` | The [browser console](../vantage/browser-console.md) |
|
||||
| `oidc` | Per-instance [single sign-on](../vantage/settings.md#single-sign-on-oidc) |
|
||||
|
||||
No tier includes them by default; you enable them on the instances that need
|
||||
them.
|
||||
|
||||
## Entitlements: desired and granted
|
||||
|
||||
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
|
||||
|
||||
An increase takes effect when payment confirms, and the entitlement is promoted
|
||||
desired → granted.
|
||||
|
||||
A **reduction** is scheduled rather than immediate: you keep what you paid for
|
||||
until the end of the term, and the portal shows the date it drops. The collapse
|
||||
happens at renewal.
|
||||
|
||||
## What a licence carries
|
||||
|
||||
Instance UUID, deployment, tier, resolved limits, features, term and expiry —
|
||||
all signed.
|
||||
|
||||
Two properties follow from that:
|
||||
|
||||
- **A licence is bound to one instance UUID.** Moving it takes a
|
||||
[relink](./self-hosted-instances.md#relinking).
|
||||
- **A licence is a snapshot.** Editing a plan later never rewrites an issued
|
||||
licence, the same way editing a workflow step never rewrites a past run.
|
||||
|
||||
Verification is local. Your instance does not call HQ to check a licence, and
|
||||
signing happens only in HQ.
|
||||
|
||||
## Expiry and grace
|
||||
|
||||
Expiry is padded with a grace period. Past that, the instance goes into degraded
|
||||
mode: it keeps running and keeps your data, but stops letting you do everything.
|
||||
|
||||
The way out is a current licence — renew or purchase, then paste it (self-hosted)
|
||||
or let it be written for you (cloud).
|
||||
|
||||
## Server limits in practice
|
||||
|
||||
When you exceed your server allowance, enrolling another one is refused. The
|
||||
existing fleet is unaffected. Raise the allowance in the portal, or remove a
|
||||
server you are not using.
|
||||
|
||||
## Legacy tiers
|
||||
|
||||
An older `self_hosted` tier is mapped forward to self-hosted Professional
|
||||
wherever it appears. Nothing needs doing about it.
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
id: people-and-roles
|
||||
title: People and roles
|
||||
sidebar_label: People and roles
|
||||
---
|
||||
|
||||
Two separate things live here: who is in your **account**, and who has access to
|
||||
each **instance**.
|
||||
|
||||
## Account members
|
||||
|
||||
**People** lists everyone in the account.
|
||||
|
||||
| Role | Can |
|
||||
| --- | --- |
|
||||
| `owner` | Everything, including billing |
|
||||
| `admin` | Invite, create instances, grant instance access |
|
||||
| `member` | Read |
|
||||
|
||||
Owners and admins invite; billing is owner-only.
|
||||
|
||||
### Inviting someone
|
||||
|
||||
1. **People → Invite**.
|
||||
2. Enter their email and pick a role.
|
||||
3. They receive a link and set their own password at `/accept-invite`.
|
||||
|
||||
:::info Why you cannot set their password
|
||||
An invitation creates a person with an **empty password hash**, which cannot
|
||||
authenticate at all until they set one. If the inviter chose it, that password
|
||||
would be a shared credential to every instance the person is later granted
|
||||
access to.
|
||||
|
||||
The verification endpoint knows the difference: a token belonging to a
|
||||
passwordless person reports that a password is needed and is left unspent, so
|
||||
the link still works when they get to it.
|
||||
:::
|
||||
|
||||
### Removing someone
|
||||
|
||||
Removing them from the account removes their portal access. See below for what
|
||||
happens to their instance access.
|
||||
|
||||
## Instance access
|
||||
|
||||
Granting access to a **cloud** instance creates a real user inside that
|
||||
instance's control plane, with `auth_source: "hq"`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P["HQ account member"] -->|grant| U["Control-plane user<br/>auth_source: hq"]
|
||||
U --> I["The instance authenticates<br/>this user like any other"]
|
||||
```
|
||||
|
||||
The instance authenticates that user exactly as it authenticates anyone else,
|
||||
with **no runtime dependency on HQ**. Revoking deletes the row — the control
|
||||
plane has no disabled state, and a row that exists is a row that can sign in.
|
||||
|
||||
### Granting
|
||||
|
||||
On an instance record, **Members → Add**, choose an account member and an
|
||||
instance role (`owner`, `admin`, `member`).
|
||||
|
||||
One person holds at most one user per instance, so granting twice is refused
|
||||
rather than quietly creating a second user.
|
||||
|
||||
### Roles inside an instance
|
||||
|
||||
Independent of the account role. Someone can be an account `member` and an
|
||||
instance `owner`, or the reverse.
|
||||
|
||||
### Revoking
|
||||
|
||||
Removes the user from the instance immediately. Any live session ends with the
|
||||
session, since the user row backing it is gone.
|
||||
|
||||
:::warning Self-hosted instances cannot be granted from HQ
|
||||
All three member endpoints refuse when the instance is self-hosted. Manage those
|
||||
users in the instance itself, at **Settings → Access**.
|
||||
:::
|
||||
|
||||
## Passwords
|
||||
|
||||
Your HQ password is the single source of truth for every user projected from it.
|
||||
Changing it in the portal rehashes it and copies the hash to every instance you
|
||||
have been granted.
|
||||
|
||||
Propagation is best-effort and immediate; a background pass compares and repairs
|
||||
every 15 minutes, so a temporarily unreachable instance catches up on its own.
|
||||
|
||||
There is no local password-change endpoint for those users in the control plane,
|
||||
so there is never a second writer for the hash.
|
||||
|
||||
:::warning HQ-managed users are read-only in the instance
|
||||
Changing the role of, or deleting, an `hq`-sourced user inside the control plane
|
||||
is refused with `409`. Do it from the portal. The UI shows those rows read-only
|
||||
with a link back here, but the API is the boundary; the UI is the courtesy.
|
||||
:::
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
id: self-hosted-instances
|
||||
title: Self-hosted instances
|
||||
sidebar_label: Self-hosted instances
|
||||
---
|
||||
|
||||
A self-hosted instance is your install, licensed through HQ. HQ never touches
|
||||
it: it issues a signed file that your install verifies locally.
|
||||
|
||||
## Free
|
||||
|
||||
Install first, then link and claim. Step by step in
|
||||
[Claim a Free licence](../getting-started/claim-free-licence.md).
|
||||
|
||||
## Paid
|
||||
|
||||
Buying happens **before** the install has to exist, because you may well be
|
||||
buying in order to build it.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Buy in HQ"] --> B["Placeholder instance<br/>awaiting_link, no licence"]
|
||||
B --> C["Install Vantage<br/>get its instance UUID"]
|
||||
C --> D["Paste the UUID<br/>claim-link"]
|
||||
D --> E["Licence issued<br/>bound to that UUID"]
|
||||
```
|
||||
|
||||
1. **Overview → Buy self-hosted**, choose tier, term and configuration.
|
||||
2. Complete checkout. HQ creates a **placeholder** instance in state
|
||||
`awaiting_link` with no licence attached.
|
||||
3. [Install Vantage](../getting-started/self-hosted-install.md) if you have not
|
||||
already, and find its instance UUID at **Settings → Licence**.
|
||||
4. Back in HQ, open the placeholder and paste the UUID.
|
||||
5. The licence is issued, bound to that UUID. Download it and paste it into your
|
||||
install.
|
||||
|
||||
:::info Why there is a placeholder at all
|
||||
A licence binds to an instance UUID, and at the moment of payment that UUID may
|
||||
not exist yet. Issuing early would mean issuing to nothing; refusing to sell
|
||||
until you had installed would be the wrong order. The placeholder holds the
|
||||
purchase until there is something to bind to.
|
||||
:::
|
||||
|
||||
## Linking an existing install
|
||||
|
||||
If the install already exists, **Link an instance** takes the UUID directly. A
|
||||
UUID already claimed by another account is refused with a conflict.
|
||||
|
||||
## Relinking
|
||||
|
||||
Rebuilding the host produces a new instance UUID, and the old licence no longer
|
||||
matches. **Relink** moves the licence to the new UUID and reissues.
|
||||
|
||||
The number of relinks per term is capped, and the portal shows how many you have
|
||||
left. This is not meant to obstruct disaster recovery — if you have exhausted
|
||||
them for a real reason, ask support.
|
||||
|
||||
## Installing the licence
|
||||
|
||||
Paste it at **Settings → Licence** in your install. The instance verifies the
|
||||
signature and checks that the UUID matches its own.
|
||||
|
||||
Pasting works even while the current licence is expired — that endpoint is
|
||||
exempt from the licence check, because it is the route out of degraded mode.
|
||||
|
||||
## Keeping it current
|
||||
|
||||
Your install does not fetch licences. When a licence is reissued — renewal,
|
||||
configuration change, relink — download the new one from HQ and paste it in.
|
||||
|
||||
:::warning Nothing reminds your install
|
||||
The control plane knows only what its licence says. Expiry emails come from HQ,
|
||||
to the account's address. Make sure someone reads them.
|
||||
:::
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
id: index
|
||||
title: Vantage documentation
|
||||
sidebar_label: Overview
|
||||
slug: /
|
||||
---
|
||||
|
||||
# Vantage documentation
|
||||
|
||||
Vantage is a self-hosted, multi-tenant infrastructure control plane. It manages
|
||||
SSH keys, runs scripted workflows, watches services, stores secrets, opens
|
||||
browser consoles and applies OS updates across a fleet of servers.
|
||||
|
||||
A central server drives a lightweight agent installed on each managed machine.
|
||||
The agent connects **outbound only**, so managed servers need no inbound
|
||||
firewall holes.
|
||||
|
||||
## Where to start
|
||||
|
||||
| If you want to | Read |
|
||||
| --- | --- |
|
||||
| Understand what the pieces are | [What is Vantage](./getting-started/what-is-vantage.md) |
|
||||
| Run it on your own hardware | [Self-hosted install](./getting-started/self-hosted-install.md) |
|
||||
| Enrol your first machine | [Add your first server](./getting-started/first-server.md) |
|
||||
| Manage your account, licence or billing | [Vantage HQ](./hq/accounts-and-signup.md) |
|
||||
| Look something up | [Reference](./reference/environment-variables.md) |
|
||||
|
||||
## The two products
|
||||
|
||||
**Vantage** is the control plane — the thing you sign in to in order to manage
|
||||
servers. It runs either on your own infrastructure or as a cloud instance we
|
||||
run for you.
|
||||
|
||||
**Vantage HQ** is the portal at `vantage-hq.hostxtra.co.uk` where you manage the
|
||||
account behind those instances: who is on your team, which instances exist, what
|
||||
licence each one holds and how it is billed. HQ never manages your servers, and
|
||||
a Vantage instance never depends on HQ being reachable in order to run.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
id: agent-updates
|
||||
title: Agent updates
|
||||
sidebar_label: Agent updates
|
||||
---
|
||||
|
||||
Agents are versioned and released independently of the control plane, and update
|
||||
themselves on command.
|
||||
|
||||
## Checking the current version
|
||||
|
||||
Each server's detail page shows the version it reported at its last sync.
|
||||
`GET /api/agent/latest-version` reports the newest release available.
|
||||
|
||||
## Updating from the UI
|
||||
|
||||
**Servers → *a server* → Update agent** pushes `UpdateAgentCmd` with a target
|
||||
version. The agent then:
|
||||
|
||||
1. Downloads the binary for its platform from the release.
|
||||
2. Verifies the SHA-256 against `checksums.txt`.
|
||||
3. Stops itself, replaces the binary in place, and starts again.
|
||||
|
||||
`Restart=always` on the systemd unit is what makes the last step work.
|
||||
|
||||
The server briefly goes `offline` and comes back within a poll interval or two.
|
||||
|
||||
## Updating from the machine
|
||||
|
||||
There is a dynamic update script, the counterpart to the install one:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://vantage.example.com/update | bash
|
||||
```
|
||||
|
||||
```powershell
|
||||
irm https://vantage.example.com/update.ps1 | iex
|
||||
```
|
||||
|
||||
It does the same download, checksum and replace, then restarts the service. Use
|
||||
this when the control plane cannot push — for example, when the machine is
|
||||
reachable but its command stream is not.
|
||||
|
||||
## Rolling out across a fleet
|
||||
|
||||
There is no built-in bulk update. Two reasonable approaches:
|
||||
|
||||
- Update from each server's page, a few at a time.
|
||||
- Build a [workflow](../vantage/workflows.md) whose step runs the update script,
|
||||
and target it at the machines you want. That gives you ordering, failure
|
||||
handling and a log.
|
||||
|
||||
:::tip Update a canary first
|
||||
An agent that fails to start after replacing itself needs hands on that machine.
|
||||
Do one, confirm it returns to `active`, then do the rest.
|
||||
:::
|
||||
|
||||
## Version compatibility
|
||||
|
||||
The gRPC API is versioned to tolerate an agent older than the control plane. The
|
||||
reverse — an agent newer than the control plane — is not a case anyone tests.
|
||||
Upgrade the control plane first.
|
||||
|
||||
Agents report their version on every `SyncKeys`, so a fleet running mixed
|
||||
versions is visible in the server list rather than something you have to go
|
||||
looking for.
|
||||
|
||||
## If an update fails
|
||||
|
||||
| Symptom | Cause |
|
||||
| --- | --- |
|
||||
| "Checksum mismatch" | Interrupted download, or a proxy rewriting the body. Retry |
|
||||
| Downloads nothing | The machine cannot reach `gitea.hostxtra.co.uk` |
|
||||
| Service will not start afterwards | Wrong architecture binary, or the file was replaced while a different service manager held it. Reinstall with the install one-liner |
|
||||
|
||||
Reinstalling is always safe: the config file is left alone, so the agent comes
|
||||
back with the same identity and token.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
id: backups
|
||||
title: Backups
|
||||
sidebar_label: Backups
|
||||
---
|
||||
|
||||
Two things matter: **MongoDB** and **`KEY_ENCRYPTION_KEY`**. A backup missing
|
||||
either one restores to something unusable.
|
||||
|
||||
## What holds what
|
||||
|
||||
| Store | Contents | Back up |
|
||||
| --- | --- | --- |
|
||||
| MongoDB | Everything durable — servers, keys, assignments, workflows, runs, monitors, incidents, secrets, settings, audit | **Yes** |
|
||||
| Redis | Sessions only | No. Losing it signs everyone out and nothing else |
|
||||
| `./data` bind mount | Workflow run logs | Optional |
|
||||
| `KEY_ENCRYPTION_KEY` | Not stored anywhere by the app | **Yes, separately** |
|
||||
|
||||
:::danger The database alone is not a backup
|
||||
Private keys, vault secrets, OIDC client secrets and console credentials are
|
||||
encrypted with `KEY_ENCRYPTION_KEY`, which lives in your environment file and
|
||||
nowhere in the database. Restore the database without it and every one of those
|
||||
values is permanently unreadable.
|
||||
|
||||
Store the key somewhere other than the server it protects.
|
||||
:::
|
||||
|
||||
## Backing up MongoDB
|
||||
|
||||
With the bundled Mongo container:
|
||||
|
||||
```bash
|
||||
docker compose exec -T mongo mongodump --archive --gzip --db vantage \
|
||||
> /backups/vantage-$(date +%F).archive.gz
|
||||
```
|
||||
|
||||
Restoring:
|
||||
|
||||
```bash
|
||||
docker compose exec -T mongo mongorestore --archive --gzip --drop \
|
||||
< /backups/vantage-2026-07-28.archive.gz
|
||||
```
|
||||
|
||||
`--drop` replaces existing collections. Stop the `server` container first, so
|
||||
nothing writes during the restore.
|
||||
|
||||
## Backing up the environment file
|
||||
|
||||
```bash
|
||||
cp /opt/vantage/.env /secure-location/vantage.env
|
||||
```
|
||||
|
||||
Treat it as a credential in its own right — it holds the encryption key.
|
||||
|
||||
## Run logs
|
||||
|
||||
Workflow run logs live in the `./data` bind mount, not in the database. They are
|
||||
swept on the retention schedule anyway, so most people do not back them up. If
|
||||
you keep them for compliance, set retention to `0` (forever) and include the
|
||||
directory.
|
||||
|
||||
## What a restore gives you
|
||||
|
||||
Everything: fleet, keys, assignments, workflows and their history, monitors and
|
||||
incidents, secrets, settings and the audit log.
|
||||
|
||||
What it does **not** do is reconcile the world. After a restore:
|
||||
|
||||
- Agents reconnect with their existing tokens, since the token hashes are in the
|
||||
database.
|
||||
- If the restore is older than an enrolment, that server's token hash is missing
|
||||
and the agent will fail to authenticate — re-enrol it.
|
||||
- The next agent poll rewrites `authorized_keys` to match the restored desired
|
||||
state, which may remove keys added since the backup.
|
||||
|
||||
## A workable schedule
|
||||
|
||||
| What | When |
|
||||
| --- | --- |
|
||||
| MongoDB dump | Nightly, retained per your policy |
|
||||
| Environment file | On change, held in a password manager or secret store |
|
||||
| Restore rehearsal | Occasionally, into a throwaway host |
|
||||
|
||||
The rehearsal is the part that gets skipped and the part that finds the
|
||||
problems.
|
||||
|
||||
## Cloud instances
|
||||
|
||||
We back these up. You do not need to.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
id: ci-cd
|
||||
title: CI/CD
|
||||
sidebar_label: CI/CD
|
||||
---
|
||||
|
||||
Two Gitea Actions workflows: one releases agents, one builds images.
|
||||
|
||||
## Agent releases
|
||||
|
||||
Triggered by an `agent/v*` tag.
|
||||
|
||||
```bash
|
||||
git tag agent/v1.0.0 && git push origin agent/v1.0.0
|
||||
```
|
||||
|
||||
Builds `linux/amd64`, `linux/arm64` and `windows/amd64`, writes `checksums.txt`
|
||||
and creates a Gitea release. A second job on Windows packages the WiX MSI.
|
||||
|
||||
The install and update scripts read the newest `agent/v*` release from the Gitea
|
||||
API, so tagging is what makes a new agent available to every install.
|
||||
|
||||
## Image builds
|
||||
|
||||
Triggered on every push to `main`. Builds and pushes seven images: `server`,
|
||||
`web`, `site`, `sitesvc`, `admin`, `adminsite` and `docsite`.
|
||||
|
||||
:::warning Despite the name, this workflow does not deploy
|
||||
There is no SSH step. Rolling images out is a manual step on the host:
|
||||
|
||||
```bash
|
||||
cd /opt/vantage && \
|
||||
docker compose -f docker-compose.yml -f docker-compose.site.yml pull && \
|
||||
docker compose -f docker-compose.yml -f docker-compose.site.yml up -d --remove-orphans
|
||||
```
|
||||
:::
|
||||
|
||||
### Each image rebuilds only when its own inputs change
|
||||
|
||||
A `git diff` against the previous head decides. That is why the checkout uses
|
||||
`fetch-depth: 0` — a shallow clone has one commit and nothing to diff against.
|
||||
|
||||
| Image | Rebuilds when |
|
||||
| --- | --- |
|
||||
| `server` | `server/`, `shared/`, `proto/`, `go.work` |
|
||||
| `admin` | `admin/`, `shared/`, `go.work` |
|
||||
| `sitesvc` | `sitesvc/`, `shared/`, `go.work` |
|
||||
| `web` · `site` · `adminsite` · `docsite` | their own directory only |
|
||||
|
||||
`shared/` fans out to all three Go images because each of their Dockerfiles
|
||||
copies it from a root context. **If a fourth service ever imports `shared/`, it
|
||||
must be added to that list or it will ship stale.**
|
||||
|
||||
Everything rebuilds when there is no trustworthy base commit to diff against: a
|
||||
manual `workflow_dispatch`, a new branch, or a force-push whose old head is
|
||||
gone. Changing the workflow file itself also rebuilds everything, since a build
|
||||
argument is baked into each image.
|
||||
|
||||
### The gap: repository variables
|
||||
|
||||
:::danger Editing a repository variable pushes no commit, so nothing rebuilds
|
||||
Values like `API_URL`, `ADMIN_API_URL`, `HQ_URL`, `ADMIN_ENV`, `PADDLE_ENV`,
|
||||
`PADDLE_CLIENT_TOKEN`, `DOCS_URL` and `DOCS_BASE_URL` are baked into images at
|
||||
build time. After editing one, run the workflow manually — that is what
|
||||
`workflow_dispatch` is for.
|
||||
|
||||
The symptom is a frontend that keeps using the old value with no error anywhere,
|
||||
which is a long afternoon if you do not know about this.
|
||||
:::
|
||||
|
||||
The same applies to base images: a service nobody touches stops being rebuilt on
|
||||
newer base layers. A periodic manual run covers it.
|
||||
|
||||
## Secrets and variables
|
||||
|
||||
| Name | Type | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `RELEASE_TOKEN` | Secret | Gitea API token, `write:release` |
|
||||
| `REGISTRY_USER` / `REGISTRY_PASSWORD` | Secret | Registry push credentials |
|
||||
| `PADDLE_API_KEY` | Secret | Read by admin at runtime |
|
||||
| `PADDLE_WEBHOOK_SECRET` | Secret | Webhook signature verification |
|
||||
| `GITEA_HOST` / `DOCKER_HOST` | Variable | Hosts used in tags and URLs |
|
||||
| `API_URL` | Variable | Baked into `web` |
|
||||
| `HQ_URL` | Variable | Baked into `web`; empty on self-hosted |
|
||||
| `SITE_API_URL` / `SITE_CONTACT_EMAIL` | Variable | Baked into `site` |
|
||||
| `ADMIN_API_URL` | Variable | Baked into `adminsite` **and** `site` |
|
||||
| `ADMIN_ENV` | Variable | Environment badge in the portal |
|
||||
| `PADDLE_ENV` | Variable | Baked into `adminsite`, read by `admin`. Must match on both sides |
|
||||
| `PADDLE_CLIENT_TOKEN` | Variable | Browser Paddle token for checkout |
|
||||
| `DOCS_URL` / `DOCS_BASE_URL` | Variable | Baked into `docsite` |
|
||||
|
||||
Anything marked "browser-reachable" must be an origin a browser can actually
|
||||
resolve — not an internal service name. Get it wrong and every request fails at
|
||||
runtime with a not-connected panel rather than at build time.
|
||||
|
||||
## The documentation site
|
||||
|
||||
`docsite/` builds to static files and is served by nginx under `/docs` on the
|
||||
marketing host, routed by its own proxy location.
|
||||
|
||||
`DOCS_BASE_URL` has to agree with three things at once: that proxy location, the
|
||||
directory the runtime image serves from, and the value baked into the build. When
|
||||
they disagree the page loads and every stylesheet and script 404s.
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
id: upgrading
|
||||
title: Upgrading
|
||||
sidebar_label: Upgrading
|
||||
---
|
||||
|
||||
Upgrading the control plane is a pull and a recreate. Agents are versioned and
|
||||
upgraded separately — see [Agent updates](./agent-updates.md).
|
||||
|
||||
## Upgrade
|
||||
|
||||
```bash
|
||||
cd /opt/vantage
|
||||
docker compose pull
|
||||
docker compose up -d --remove-orphans
|
||||
```
|
||||
|
||||
`--remove-orphans` clears containers for services that no longer exist in the
|
||||
Compose file, which is what leaves a stale container running after a service is
|
||||
renamed or removed.
|
||||
|
||||
## What happens on boot
|
||||
|
||||
1. **Migrations** run, recording markers so each runs once.
|
||||
2. **Indexes** are ensured. Auth and settings index builders are fatal on
|
||||
failure; secret and workflow ones only warn.
|
||||
3. **Default steps** are reseeded from the image, overwriting the `default`
|
||||
library — which is why those steps are read-only.
|
||||
|
||||
Watch it:
|
||||
|
||||
```bash
|
||||
docker compose logs -f server
|
||||
```
|
||||
|
||||
## Before you upgrade
|
||||
|
||||
- **Back up MongoDB.** See [Backups](./backups.md). Migrations are one-way.
|
||||
- **Read the release notes** for anything about migrations or environment
|
||||
variables.
|
||||
- **Check your `.env`** still supplies everything required. A newly required
|
||||
variable stops the boot rather than defaulting to something unsafe.
|
||||
|
||||
## Downgrading
|
||||
|
||||
There is no automatic downgrade. Migrations do not roll back, so returning to an
|
||||
older image means restoring the database backup taken before the upgrade. This
|
||||
is the reason the backup is not optional.
|
||||
|
||||
## Zero-downtime
|
||||
|
||||
The stack is not designed for it. `docker compose up -d` recreates the server
|
||||
container, which is a short interruption:
|
||||
|
||||
- Agents reconnect on their own — they retry, and the poll loop is idempotent.
|
||||
- Workflow runs in progress lose their command stream. Steps already dispatched
|
||||
finish on the agent, but their results have nowhere to go. **Do not upgrade
|
||||
during a run.**
|
||||
- Sessions survive, because they live in Redis rather than in the server.
|
||||
|
||||
## Upgrading the hosted deployment
|
||||
|
||||
Both Compose files, together:
|
||||
|
||||
```bash
|
||||
cd /opt/vantage
|
||||
docker compose -f docker-compose.yml -f docker-compose.site.yml pull
|
||||
docker compose -f docker-compose.yml -f docker-compose.site.yml up -d --remove-orphans
|
||||
```
|
||||
|
||||
CI builds and pushes images but does **not** deploy them; rolling out is this
|
||||
manual step. See [CI/CD](./ci-cd.md).
|
||||
|
||||
## After upgrading
|
||||
|
||||
- Confirm every service is `running`.
|
||||
- Confirm servers return to `active` within a couple of poll intervals.
|
||||
- Open a page that touches encryption — a secret group — to confirm
|
||||
`KEY_ENCRYPTION_KEY` came through.
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
id: agent-config
|
||||
title: Agent configuration
|
||||
sidebar_label: Agent config
|
||||
---
|
||||
|
||||
The agent reads no environment variables. Everything is in one YAML file.
|
||||
|
||||
## Location
|
||||
|
||||
| Platform | Path |
|
||||
| --- | --- |
|
||||
| Linux | `/etc/vantage/config.yaml` |
|
||||
| Windows | `%ProgramData%\vantage\config.yaml` |
|
||||
|
||||
Directory `0700`, file `0600`. The install script sets both.
|
||||
|
||||
## Contents
|
||||
|
||||
```yaml
|
||||
server_url: "vantage.yourdomain.com:9090"
|
||||
server_id: "<uuid>"
|
||||
pre_reg_token: "<token>" # removed after the first successful Register()
|
||||
agent_token: "" # written by the agent after Register()
|
||||
poll_interval: 30s
|
||||
tls: true
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `server_url` | `host:port` of the gRPC endpoint. Comes from the server's `GRPC_HOST` |
|
||||
| `server_id` | The identity issued when the enrolment was created |
|
||||
| `pre_reg_token` | Single-use, one hour. Cleared once registration succeeds |
|
||||
| `agent_token` | The permanent credential, written by the agent itself |
|
||||
| `poll_interval` | How often `SyncKeys` runs. Default `30s` |
|
||||
| `tls` | Whether to use TLS. Leave `true` |
|
||||
|
||||
:::danger This file is the credential
|
||||
`agent_token` is plaintext here and nowhere else — the control plane holds only
|
||||
its SHA-256. Anyone who can read this file can act as this agent. That is why
|
||||
it is `0600` and the directory is `0700`.
|
||||
:::
|
||||
|
||||
## Startup sequence
|
||||
|
||||
```
|
||||
1. Load the config
|
||||
2. pre_reg_token present → Register() → save agent_token,
|
||||
clear pre_reg_token, reconnect
|
||||
3. Start goroutines: command stream · hourly update check ·
|
||||
inventory · monitors
|
||||
4. Enter the SyncKeys poll loop
|
||||
```
|
||||
|
||||
## The poll loop
|
||||
|
||||
```
|
||||
1. SyncKeys(server_id, agent_token, agent_version)
|
||||
2. Non-Linux hosts stop here — Windows agents register and heartbeat only
|
||||
3. Diff the desired keys against /root/.ssh/authorized_keys;
|
||||
unchanged → write nothing
|
||||
4. Changed → write a temp file, os.Rename() over the real one, chmod 0600
|
||||
```
|
||||
|
||||
## Service management
|
||||
|
||||
### Linux
|
||||
|
||||
Unit at `/etc/systemd/system/vantage-agent.service`, `Restart=always`, running
|
||||
as root.
|
||||
|
||||
```bash
|
||||
systemctl status vantage-agent
|
||||
systemctl restart vantage-agent
|
||||
journalctl -u vantage-agent -f
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
A service registered through NSSM, or installed by the MSI that CI builds.
|
||||
|
||||
```powershell
|
||||
Get-Service vantage-agent
|
||||
Restart-Service vantage-agent
|
||||
```
|
||||
|
||||
## Command-line flags
|
||||
|
||||
```
|
||||
vantage-agent -generate-key
|
||||
```
|
||||
|
||||
Generates a keypair locally. Normal operation takes no flags.
|
||||
|
||||
## Moving an agent to a new control plane
|
||||
|
||||
Change `server_url`, clear `agent_token`, set a fresh `pre_reg_token` from a new
|
||||
enrolment, and restart. The old control plane still holds a server record that
|
||||
will go `offline`; delete it there.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
systemctl disable --now vantage-agent
|
||||
rm -f /usr/local/bin/vantage-agent /etc/systemd/system/vantage-agent.service
|
||||
rm -rf /etc/vantage
|
||||
systemctl daemon-reload
|
||||
```
|
||||
|
||||
Keys already written to `authorized_keys` remain on disk — the agent is no
|
||||
longer running to remove them. Revoke first if that matters.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
id: environment-variables
|
||||
title: Environment variables
|
||||
sidebar_label: Environment variables
|
||||
---
|
||||
|
||||
Everything the control plane reads from the environment, and what happens when
|
||||
it is absent.
|
||||
|
||||
## Server
|
||||
|
||||
| Name | Required | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `GRPC_HOST` | **yes** | — | The `host:port` agents dial. Boot fails without it. There is deliberately no fallback to the web host: that would hand every agent a port that does not speak gRPC |
|
||||
| `MONGO_URI` | no | `mongodb://localhost:27017` | The database name is taken from the URI path, falling back to `vantage`. There is no separate `MONGO_DB` |
|
||||
| `REDIS_ADDR` | no | `localhost:6379` | Sessions only |
|
||||
| `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 | `gitea.example.com` | Used to build the install scripts and agent download URLs. The default is a placeholder that will not resolve |
|
||||
| `GUACD_ADDR` | no | `guacd:4822` | The [browser console](../vantage/browser-console.md) daemon |
|
||||
| `APP_ROOT_LABEL` | no | `vantage` | The app root label for the host and session organisation guard |
|
||||
| `VANTAGE_WORKFLOW_LOG_DIR` | no | — | Where workflow run logs are written |
|
||||
| `VANTAGE_DEFAULT_STEPS_DIR` | no | baked into the image | Where the seeded step library is read from |
|
||||
| `VANTAGE_DEPLOYMENT` | no | self-hosted | Set to `cloud` on a cloud instance. Governs whether a licence may be pasted |
|
||||
| `VANTAGE_LICENSE` | no | — | A licence blob, used **only** when the instance has no stored one |
|
||||
| `FREE_INSTANCE_REAP_AFTER` | no | empty | How long past a Free licence's expiry before the instance and all its data are deleted |
|
||||
|
||||
:::danger `KEY_ENCRYPTION_KEY` has no recovery path
|
||||
It encrypts SSH private keys, vault secrets, OIDC client secrets and console
|
||||
credentials. Lose it and all of them are unreadable. Back it up separately from
|
||||
the database it protects.
|
||||
:::
|
||||
|
||||
:::warning `FREE_INSTANCE_REAP_AFTER` empty means disabled, and empty is the default
|
||||
That is the correct value for a self-hosted install, which must never reap. It
|
||||
is set only on the hosted deployment.
|
||||
:::
|
||||
|
||||
:::info A wrong `APP_ROOT_LABEL` fails quietly
|
||||
It does not error. It simply stops matching, and the host/session guard stops
|
||||
protecting anything.
|
||||
:::
|
||||
|
||||
### Not configurable
|
||||
|
||||
The HTTP port (`8080`) and the gRPC port (`9090`) are fixed in the server. The
|
||||
`HTTP_PORT` and `GRPC_PORT` entries in the shipped Compose file are inert —
|
||||
remap with Docker's port publishing instead.
|
||||
|
||||
## Agent
|
||||
|
||||
The agent reads no environment variables. Everything is in its
|
||||
[config file](./agent-config.md).
|
||||
|
||||
## Hosted-only services
|
||||
|
||||
These run only on the hosted deployment, from
|
||||
`deploy/docker-compose.site.yml`. A self-hosted install runs none of them.
|
||||
|
||||
### sitesvc — the public contact form
|
||||
|
||||
| Name | Required | Notes |
|
||||
| --- | --- | --- |
|
||||
| `MONGO_URI` | yes | Must point at the control plane's database. Refuses to start against a database that has not run the instances migration. The database name is read from the URI path; a URI without one is refused rather than defaulted |
|
||||
| `SMTP_HOST`, `SMTP_FROM` | yes | Without them the contact form answers `503` rather than silently dropping messages |
|
||||
| `SMTP_TO` | no | Defaults to `support@hostxtra.co.uk` |
|
||||
| `SMTP_PORT` | no | Defaults to `587`; `465` uses implicit TLS |
|
||||
| `SMTP_USERNAME`, `SMTP_PASSWORD` | no | Auth is skipped when the username is empty |
|
||||
| `SITE_ORIGIN` | yes in practice | Comma-separated allowed origins. Unset refuses every cross-origin browser request |
|
||||
| `TRUST_PROXY` | no | Only `true` behind a proxy that overwrites `X-Forwarded-For`, or clients spoof past the rate limiter |
|
||||
|
||||
### admin — the licensing authority
|
||||
|
||||
| Name | Required | Notes |
|
||||
| --- | --- | --- |
|
||||
| `ADMIN_MONGO_URI` | yes | Admin's own database |
|
||||
| `CONTROL_MONGO_URI` | yes | The control plane's database, for licence injection and user projection |
|
||||
| `LICENSE_SIGNING_KEY` | yes | **The only service that ever holds this.** Never add it to the server, and never add admin to the self-hosted Compose file |
|
||||
| `REDIS_ADDR`, `REDIS_USERNAME`, `REDIS_PASSWORD` | yes | Admin uses an external Redis; the base Compose file hardcodes `redis:6379` for the server, so these reach admin only |
|
||||
| `ADMIN_ORIGIN` | yes | Comma-separated browser origins that call admin. See the warning below |
|
||||
| `PADDLE_API_KEY` | yes | Boot-required |
|
||||
| `PADDLE_WEBHOOK_SECRET` | yes | Boot-required. An unverified webhook endpoint is one anyone can issue licences through |
|
||||
| `PADDLE_ENV` | yes | `sandbox` or `production`. Selects which catalogue price IDs are served, and must match the value baked into the portal build |
|
||||
| `SMTP_*` | yes in practice | Account, licence and billing email |
|
||||
| `PUBLIC_URL`, `APP_LOGIN_URL` | yes in practice | Used in links inside emails |
|
||||
| `FREE_INSTANCE_REAP_AFTER` | yes | Must match the control plane's value. Admin only uses it to name the date in warning emails; the control plane performs the delete |
|
||||
|
||||
:::warning A missing `ADMIN_ORIGIN` entry produces no error anywhere
|
||||
The CORS layer simply omits the allow-origin header and still answers the
|
||||
preflight with `204`. The browser blocks the request and **admin logs nothing at
|
||||
all**. The symptom is a preflight failure on an endpoint that works perfectly
|
||||
under `curl`.
|
||||
:::
|
||||
|
||||
## Build-time variables
|
||||
|
||||
These are baked into frontend images at build time, not read at runtime.
|
||||
Changing one requires rebuilding that image — and because editing a CI variable
|
||||
pushes no commit, nothing rebuilds on its own. See [CI/CD](../operations/ci-cd.md).
|
||||
|
||||
| Name | Baked into |
|
||||
| --- | --- |
|
||||
| `API_URL` | `web` |
|
||||
| `HQ_URL` | `web` |
|
||||
| `SITE_API_URL`, `SITE_CONTACT_EMAIL` | `site` |
|
||||
| `ADMIN_API_URL` | `adminsite` **and** `site` |
|
||||
| `ADMIN_ENV`, `PADDLE_CLIENT_TOKEN`, `PADDLE_ENV` | `adminsite` |
|
||||
| `DOCS_URL`, `DOCS_BASE_URL` | `docsite` |
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
id: grpc-api
|
||||
title: gRPC API
|
||||
sidebar_label: gRPC API
|
||||
---
|
||||
|
||||
The agent-facing API, on port `9090`, over TLS. Agents dial **out** to it;
|
||||
nothing dials an agent.
|
||||
|
||||
## Service
|
||||
|
||||
```protobuf
|
||||
service Vantage {
|
||||
rpc Register(RegisterRequest) returns (RegisterResponse);
|
||||
rpc SyncKeys(SyncRequest) returns (SyncResponse);
|
||||
rpc UploadGeneratedKey(UploadKeyRequest) returns (UploadKeyResponse);
|
||||
rpc ReportUpdates(ReportUpdatesRequest) returns (ReportUpdatesResponse);
|
||||
rpc ReportInventory(InventoryReport) returns (InventoryReportResponse);
|
||||
rpc SyncMonitors(SyncMonitorsRequest) returns (SyncMonitorsResponse);
|
||||
rpc ReportChecks(ReportChecksRequest) returns (ReportChecksResponse);
|
||||
rpc CommandStream(stream AgentMessage) returns (stream ServerCommand);
|
||||
}
|
||||
```
|
||||
|
||||
The full message definitions live in `proto/vantage/v1/vantage.proto`.
|
||||
|
||||
## Authentication
|
||||
|
||||
`Register` presents the single-use, one-hour pre-registration token and receives
|
||||
a permanent agent token. Every other call presents that agent token.
|
||||
|
||||
The control plane stores only the SHA-256 of the agent token. The plaintext
|
||||
exists in the agent's `0600` config file and nowhere else, so a token cannot be
|
||||
read back out of the control plane.
|
||||
|
||||
## Unary calls
|
||||
|
||||
| RPC | Direction | Frequency |
|
||||
| --- | --- | --- |
|
||||
| `Register` | once, at enrolment | once |
|
||||
| `SyncKeys` | agent asks for desired key state | every 30s (`poll_interval`) |
|
||||
| `UploadGeneratedKey` | agent returns a keypair it generated | on demand |
|
||||
| `ReportUpdates` | pending OS package updates | hourly |
|
||||
| `ReportInventory` | CPU, memory, disk, kernel | metrics 30s, static 15 min |
|
||||
| `SyncMonitors` | agent asks which checks it should run | periodically |
|
||||
| `ReportChecks` | agent returns check results | after each check cycle |
|
||||
|
||||
`SyncKeys` doubles as the heartbeat. A server that stops calling it is marked
|
||||
`offline` by a sweep that runs every two minutes.
|
||||
|
||||
## The command stream
|
||||
|
||||
`CommandStream` is the only streaming RPC and the only push path.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Agent
|
||||
participant S as Server
|
||||
A->>S: AgentReady (authenticate)
|
||||
S-->>A: ServerCommand (RunStepCmd)
|
||||
A-->>S: StepOutputChunk (repeated)
|
||||
A-->>S: StepResult
|
||||
S-->>A: ServerCommand (CleanupWorkspaceCmd)
|
||||
A-->>S: CommandResult
|
||||
```
|
||||
|
||||
The agent authenticates once with `AgentReady`, then the server pushes commands
|
||||
and the agent replies with `CommandResult`, `StepResult` or `StepOutputChunk`.
|
||||
|
||||
### Commands
|
||||
|
||||
| Command | Effect |
|
||||
| --- | --- |
|
||||
| `GenerateKeyCmd` | Generate an SSH keypair on the machine |
|
||||
| `DeleteKeyCmd` | Remove a generated key by label |
|
||||
| `UpdateAgentCmd` | Download and replace the agent binary with a target version |
|
||||
| `ApplyUpdatesCmd` | Apply pending OS package updates |
|
||||
| `RunStepCmd` | Execute one workflow step |
|
||||
| `CleanupWorkspaceCmd` | Recursively remove the run's working directory |
|
||||
|
||||
## Why poll for keys and push for commands
|
||||
|
||||
A 30-second delay on a key change is fine, and polling needs no reconnection
|
||||
logic to survive a dropped link. Clicking **Run** on a workflow and waiting up
|
||||
to 30 seconds is not fine. Hence one of each.
|
||||
|
||||
## Network requirements
|
||||
|
||||
Every managed machine needs outbound TCP to `GRPC_HOST`. Nothing needs to reach
|
||||
the machine. Watch for middleboxes that permit the short `Register` call but
|
||||
drop the long-lived command stream — that failure looks like a server that
|
||||
registers and then goes offline.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
id: ports-and-networking
|
||||
title: Ports and networking
|
||||
sidebar_label: Ports and networking
|
||||
---
|
||||
|
||||
## Control plane ports
|
||||
|
||||
| Port | Service | Who connects | Expose publicly |
|
||||
| --- | --- | --- | --- |
|
||||
| `3000` | web | Browsers, via your reverse proxy | Yes, behind TLS |
|
||||
| `8080` | server REST | The web app | No |
|
||||
| `9090` | server gRPC | Agents | **Yes** |
|
||||
| `4822` | guacd | The server | No |
|
||||
| `27017` | MongoDB | The server | No |
|
||||
| `6379` | Redis | The server | No |
|
||||
|
||||
## Hosted-only ports
|
||||
|
||||
Only on the hosted deployment, from `deploy/docker-compose.site.yml`.
|
||||
|
||||
| Port | Service |
|
||||
| --- | --- |
|
||||
| `3003` | marketing site |
|
||||
| `3004` | HQ portal |
|
||||
| `3005` | this documentation site |
|
||||
| `8082` | sitesvc |
|
||||
| `8083` | admin |
|
||||
|
||||
## Direction of travel
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
B["Browser"] -->|HTTPS| P["Reverse proxy"]
|
||||
P --> W["web :3000"]
|
||||
W --> S["server :8080"]
|
||||
A["Agent on a managed server"] -->|"gRPC/TLS :9090, outbound"| S
|
||||
S --> G["guacd :4822"]
|
||||
G -->|"SSH / RDP / VNC"| T["Target machine"]
|
||||
```
|
||||
|
||||
Two things are worth reading off that diagram.
|
||||
|
||||
**Agents connect outbound.** No inbound rule is needed on a managed server, and
|
||||
NAT is not an obstacle. The only requirement is that the machine can reach
|
||||
`GRPC_HOST`.
|
||||
|
||||
**The console does not use the agent.** guacd connects directly to the target on
|
||||
the protocol port. A machine reachable only by its agent — behind NAT, on a
|
||||
private subnet — cannot be consoled, even though every other feature works.
|
||||
|
||||
## What to open
|
||||
|
||||
### On your firewall, inbound to the control plane
|
||||
|
||||
- Your web port, from wherever people are.
|
||||
- `9090`, from every network holding managed machines.
|
||||
|
||||
### Outbound from the control plane
|
||||
|
||||
- `gitea.hostxtra.co.uk`, for agent releases and version checks.
|
||||
- Anything a server-run [monitor](../vantage/monitors.md) checks.
|
||||
- SMTP, if you use an SMTP notification channel.
|
||||
- Protocol ports on machines you intend to console.
|
||||
|
||||
### Outbound from a managed machine
|
||||
|
||||
- `GRPC_HOST`.
|
||||
- `gitea.hostxtra.co.uk`, for install and self-update.
|
||||
- Its package mirrors, for OS updates.
|
||||
|
||||
## TLS
|
||||
|
||||
Terminate TLS for the web UI at your reverse proxy.
|
||||
|
||||
gRPC on `9090` is reached directly by agents with `tls: true`, so that port needs
|
||||
a valid certificate for the name in `GRPC_HOST`. If you proxy it, the proxy must
|
||||
speak HTTP/2 end to end — many do not by default, and the symptom is agents that
|
||||
register and then fail to hold the command stream.
|
||||
|
||||
## Reverse proxy notes
|
||||
|
||||
- Point the proxy at `web:3000`. The web app reaches the REST API internally, so
|
||||
`8080` does not need publishing.
|
||||
- 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
|
||||
truncates live logs while the run itself continues.
|
||||
|
||||
## Air-gapped and restricted networks
|
||||
|
||||
The control plane needs outbound access to fetch agent releases. Managed
|
||||
machines need it too, unless you distribute the agent binary yourself and write
|
||||
the config by hand — the install script's only job is to do those two things.
|
||||
|
||||
Licence verification is entirely local, so a licensed install works with no
|
||||
outbound access to HQ at all.
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
id: rest-api
|
||||
title: REST API
|
||||
sidebar_label: REST API
|
||||
---
|
||||
|
||||
The control plane's HTTP API, on port `8080`. The web UI is a client of it and
|
||||
has no privileges it does not.
|
||||
|
||||
## Authentication
|
||||
|
||||
Most endpoints take a session: an opaque 32-byte token in the `km_session`
|
||||
cookie, with the body in Redis for 24 hours.
|
||||
|
||||
One endpoint takes a bearer token instead — the External Secrets Operator read
|
||||
path.
|
||||
|
||||
## Unauthenticated
|
||||
|
||||
```
|
||||
GET /install /install.ps1 # dynamic agent install scripts
|
||||
GET /update /update.ps1
|
||||
GET /auth/bootstrap-status
|
||||
POST /auth/bootstrap /auth/login /auth/logout
|
||||
GET /auth/me /auth/oidc/start /auth/oidc/callback
|
||||
GET /api/secrets/:group/values # bearer token (ESO)
|
||||
```
|
||||
|
||||
`/install` and `/install.ps1` take `server_id` and `token` as query parameters
|
||||
and return a shell script with the newest agent version substituted in.
|
||||
|
||||
`POST /auth/bootstrap` works only while the database has no users.
|
||||
|
||||
## Session-authenticated, under `/api`
|
||||
|
||||
### Servers
|
||||
|
||||
```
|
||||
GET,POST /servers
|
||||
GET,POST /servers/new
|
||||
GET,DELETE /servers/:id
|
||||
POST /servers/:id/generate-key
|
||||
POST /servers/:id/update-agent
|
||||
POST /servers/:id/apply-updates
|
||||
```
|
||||
|
||||
### Keys
|
||||
|
||||
```
|
||||
GET,POST /keys
|
||||
GET,DELETE /keys/:id
|
||||
GET /keys/:id/private-key
|
||||
POST /keys/:id/assign
|
||||
DELETE /keys/:id/assign/:serverId
|
||||
```
|
||||
|
||||
### Workflows and steps
|
||||
|
||||
```
|
||||
GET,POST /steps
|
||||
PUT,DELETE /steps/:id
|
||||
GET /steps/:id/export
|
||||
POST /steps/import · /steps/seed-defaults · /steps/parse
|
||||
GET /steps/usage
|
||||
GET,POST /workflows
|
||||
GET,PUT,DELETE /workflows/:id
|
||||
POST /workflows/:id/run
|
||||
GET /workflows/:id/runs
|
||||
GET /runs/:runId
|
||||
POST /runs/:runId/cancel
|
||||
GET /runs/:runId/servers/:serverId/logs
|
||||
GET /runs/:runId/servers/:serverId/logs/stream
|
||||
```
|
||||
|
||||
`PUT` and `DELETE` on a step whose source is `default` answer `409`. See
|
||||
[Workflows](../vantage/workflows.md#default-steps).
|
||||
|
||||
### Monitors and channels
|
||||
|
||||
```
|
||||
GET,POST /monitors
|
||||
GET,PUT,DELETE /monitors/:id
|
||||
GET /monitors/:id/incidents · /monitors/:id/uptime
|
||||
GET,POST /channels
|
||||
PUT,DELETE /channels/:id
|
||||
POST /channels/:id/test
|
||||
```
|
||||
|
||||
### Secrets
|
||||
|
||||
```
|
||||
GET,POST /secrets
|
||||
GET,PUT,DELETE /secrets/:group
|
||||
POST /secrets/:group/reveal
|
||||
DELETE /secrets/:group/:key
|
||||
```
|
||||
|
||||
### Console
|
||||
|
||||
```
|
||||
POST /console/connect
|
||||
GET /console/tunnel # websocket
|
||||
```
|
||||
|
||||
### Other
|
||||
|
||||
```
|
||||
GET /audit
|
||||
GET /agent/latest-version
|
||||
GET,PUT /settings (owner|admin)
|
||||
POST /settings/secrets-token (owner|admin)
|
||||
GET /license
|
||||
POST /license (self-hosted only)
|
||||
GET,POST /org/users
|
||||
PUT /org/users/:id/role
|
||||
DELETE /org/users/:id
|
||||
GET,PUT /org/oidc (owner|admin)
|
||||
```
|
||||
|
||||
## Notable refusals
|
||||
|
||||
| Endpoint | Condition | Status |
|
||||
| --- | --- | --- |
|
||||
| `POST /license` | deployment is `cloud` | `409 cloud_managed` |
|
||||
| `PUT,DELETE /steps/:id` | the step's source is `default` | `409` |
|
||||
| `PUT /org/users/:id/role`, `DELETE /org/users/:id` | the user's auth source is `hq` | `409` |
|
||||
|
||||
`POST /license` is exempt from the licence check, so pasting a valid licence
|
||||
works while the current one is expired — that is the way out of degraded mode.
|
||||
|
||||
## Multi-tenancy
|
||||
|
||||
Every request is scoped to the instance resolved from the session. On a
|
||||
multi-tenant deployment, a request arriving at `<slug>.vantage.<tld>` also has
|
||||
its host checked against the session's instance, and a mismatch is rejected.
|
||||
|
||||
## Errors
|
||||
|
||||
Errors are JSON with an `error` field. Customer-facing endpoints in the HQ API
|
||||
answer `404` rather than `403` for another account's resource, because a `403`
|
||||
confirms the resource exists; the control plane's own API is single-tenant per
|
||||
session and does not need that distinction.
|
||||
|
||||
## Admin API
|
||||
|
||||
The HQ service has its own API, its own database and its own session cookie
|
||||
(`admin_session`) on port `8083`. It is documented in the
|
||||
[Vantage HQ](../hq/accounts-and-signup.md) section rather than here; the two
|
||||
services share no session and no authentication.
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
id: troubleshooting
|
||||
title: Troubleshooting
|
||||
sidebar_label: Troubleshooting
|
||||
---
|
||||
|
||||
Symptoms, in the order people hit them.
|
||||
|
||||
## The server will not start
|
||||
|
||||
**Exits immediately on boot.** Almost always a missing `GRPC_HOST` — the server
|
||||
refuses to start rather than guess a value that would break every agent later.
|
||||
|
||||
**Fails during index creation.** The auth and settings index builders are fatal
|
||||
on failure by design: those unique indexes are what enforce tenant isolation,
|
||||
so starting without them is worse than not starting. Check the MongoDB user's
|
||||
permissions and whether a conflicting index already exists.
|
||||
|
||||
**Starts, but every secret operation errors.** `KEY_ENCRYPTION_KEY` is missing
|
||||
or is not 64 hex characters.
|
||||
|
||||
## Nobody can sign in
|
||||
|
||||
**`/setup` appears when users already exist.** The server is pointed at a
|
||||
different database than you think. Check the database name in `MONGO_URI` —
|
||||
it comes from the URI path, not a separate variable.
|
||||
|
||||
**Sessions do not stick.** Redis is unreachable, or the cookie is being dropped
|
||||
because the site is served over plain HTTP.
|
||||
|
||||
**"Wrong organisation" style rejections.** The host and session guard is
|
||||
comparing the request host's label against the session's organisation. Check
|
||||
`APP_ROOT_LABEL`.
|
||||
|
||||
**OIDC redirects and then fails.** The callback URL registered with the provider
|
||||
must match exactly. Keep one local owner account so a broken provider is not a
|
||||
lockout.
|
||||
|
||||
## A server never becomes active
|
||||
|
||||
Work through it in this order:
|
||||
|
||||
1. Is the agent running? `systemctl status vantage-agent`.
|
||||
2. What does it say? `journalctl -u vantage-agent -f`.
|
||||
3. Can that machine reach the endpoint? Test `GRPC_HOST` **from the machine**,
|
||||
not from the control plane host.
|
||||
4. Was the token already used or expired? It is single-use and lives one hour —
|
||||
create a fresh enrolment rather than reusing the old command.
|
||||
|
||||
| Symptom | Cause |
|
||||
| --- | --- |
|
||||
| Registers, then goes `offline` within minutes | Something permits the short `Register` call but drops the long-lived stream. Usually a proxy or idle-timeout middlebox |
|
||||
| Stays `pending` forever | Registration never happened. Token spent, or the endpoint unreachable |
|
||||
| Flaps between `active` and `offline` | Intermittent path, or a poll interval longer than the offline threshold |
|
||||
|
||||
Remember the offline sweep runs every two minutes, so status is never
|
||||
instantaneous.
|
||||
|
||||
## Keys are not appearing on a machine
|
||||
|
||||
- **It is a Windows server.** Key management is Linux-only, by design.
|
||||
- **The agent is not running.** Nothing polls, nothing writes.
|
||||
- **The key is assigned but revoked.** Revocation is soft; check the assignment
|
||||
state rather than the key.
|
||||
- **Someone edited `authorized_keys` by hand.** The agent rewrites the file to
|
||||
match the desired set; hand-added keys disappear on the next change.
|
||||
|
||||
## A workflow run fails or hangs
|
||||
|
||||
- **Hangs at dispatch.** The target's command stream is not connected — the
|
||||
server may be `offline`.
|
||||
- **Fails immediately with an interpreter error.** A bash step on a Windows
|
||||
target, or PowerShell on Linux.
|
||||
- **A value does not reach the next step.** Values pass through the file at
|
||||
`$WORKFLOW_ENV`, one `KEY=value` per line. Declaring an output does not export
|
||||
it.
|
||||
- **A secret is empty.** The group is not in the step's `secret_refs`, or the
|
||||
key name differs from the environment variable you are reading.
|
||||
- **Logs stop mid-run.** A reverse proxy read timeout cut the stream. The run
|
||||
itself continues; reload the page.
|
||||
|
||||
## The console will not connect
|
||||
|
||||
| Symptom | Cause |
|
||||
| --- | --- |
|
||||
| Connects, then closes at once | guacd unreachable. Check `GUACD_ADDR` and that the container is running |
|
||||
| SSH rejects the key | The stored key has no private half, or is not on the target |
|
||||
| RDP fails on retry | Credentials are single-use and consumed at tunnel open — enter them again |
|
||||
| Hangs at "connecting" | The **control plane** cannot reach the target on the protocol port. The agent's reachability is irrelevant here |
|
||||
| Fails only in production | The reverse proxy is not forwarding WebSocket upgrade headers |
|
||||
|
||||
## Monitors report down when the service is up
|
||||
|
||||
- The check is running from the control plane and the endpoint is only reachable
|
||||
internally. Switch the runner to an agent on a machine that can see it.
|
||||
- The keyword no longer appears in the response body.
|
||||
- Retries are `0`, so a single dropped packet flips the state.
|
||||
|
||||
## Notifications are not arriving
|
||||
|
||||
Use the channel **Test** button — it goes through the real delivery path, so a
|
||||
test that arrives proves credentials, network path and destination.
|
||||
|
||||
If the test fails: a webhook returning 300 or above counts as a failure, SMTP
|
||||
needs `host`, `port`, `from` and `to`, and Telegram needs both `token` and
|
||||
`chat_id`.
|
||||
|
||||
## Licence problems
|
||||
|
||||
| Symptom | Cause |
|
||||
| --- | --- |
|
||||
| `409 cloud_managed` when pasting | It is a cloud instance. Licences are written by HQ; there is nothing to paste |
|
||||
| Licence rejected as not matching | It is bound to a different instance UUID. Relink in HQ |
|
||||
| Instance degraded despite a valid-looking licence | It has expired past its grace period. Pasting still works — that endpoint stays available specifically so it can |
|
||||
| Cannot enrol another server | The server allowance is reached. Raise it in HQ or remove one |
|
||||
|
||||
## HQ portal problems
|
||||
|
||||
**A request fails in the browser but works under `curl`.** The browser origin is
|
||||
missing from `ADMIN_ORIGIN`. This produces no log line at all in admin — the
|
||||
preflight is answered `204` without the allow-origin header, and the browser
|
||||
blocks the real request.
|
||||
|
||||
**A price or plan looks wrong after an edit.** Repository variables are baked
|
||||
into images at build time and editing one pushes no commit, so nothing rebuilds.
|
||||
Trigger the build manually. See [CI/CD](../operations/ci-cd.md).
|
||||
|
||||
## Gathering information before asking for help
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose logs --tail=200 server
|
||||
journalctl -u vantage-agent --no-pager -n 200 # on the affected machine
|
||||
```
|
||||
|
||||
Include your instance UUID from **Settings → Licence** — it is the reference
|
||||
support works from.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
id: audit-log
|
||||
title: Audit log
|
||||
sidebar_label: Audit log
|
||||
---
|
||||
|
||||
Every mutating API path writes an audit event. The log is at **Audit**.
|
||||
|
||||
## What an event carries
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| Action | A dotted name, e.g. `server.created`, `settings.updated` |
|
||||
| Actor | Who did it |
|
||||
| Target | The object acted on |
|
||||
| Detail | A short human-readable note |
|
||||
| Time | When |
|
||||
|
||||
## What is recorded
|
||||
|
||||
Creation, modification and deletion across the product: servers and enrolments,
|
||||
keys and assignments, workflow and step changes, runs triggered, monitors and
|
||||
channels, secret groups and reveals, console sessions opened, settings and
|
||||
member changes, licence installs.
|
||||
|
||||
Reads are not recorded, with one deliberate exception: **revealing a secret**
|
||||
writes an event, because reading that particular thing is an act rather than a
|
||||
lookup.
|
||||
|
||||
## What is not recorded
|
||||
|
||||
- Sign-ins and sign-out.
|
||||
- Anything inside a console session.
|
||||
- Step output. That lives in the run log, kept under the workflow retention
|
||||
setting rather than with the audit log.
|
||||
|
||||
## Retention
|
||||
|
||||
Audit events are not swept by the workflow log retention setting — that setting
|
||||
governs run logs only. Audit history stays until the instance does.
|
||||
|
||||
:::warning It is a log, not a control
|
||||
The audit log tells you what happened. It does not restrict what can happen, and
|
||||
an admin can do anything an admin can do. Use roles for restriction and the log
|
||||
for accountability.
|
||||
:::
|
||||
|
||||
## Getting events out
|
||||
|
||||
`GET /api/audit` returns recent events as JSON and accepts a `limit`. There is
|
||||
no streaming or push export; if you need events in a SIEM, poll that endpoint.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
id: browser-console
|
||||
title: Browser console
|
||||
sidebar_label: Browser console
|
||||
---
|
||||
|
||||
An SSH, RDP or VNC session in a browser tab, with no client software and no
|
||||
inbound port on the target beyond the one the protocol already uses.
|
||||
|
||||
Protocol handling is Apache Guacamole's — the control plane proxies a WebSocket
|
||||
to a **guacd** daemon and manages credentials around it.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `guacd` running and reachable from the server. The bundled Compose stack
|
||||
includes it; `GUACD_ADDR` defaults to `guacd:4822`.
|
||||
- `KEY_ENCRYPTION_KEY` set, since every credential involved is stored encrypted.
|
||||
- Network reachability **from the control plane to the target** on the protocol
|
||||
port. This is the one part of Vantage that is not agent-mediated: guacd
|
||||
connects directly, so a machine reachable only by its agent cannot be
|
||||
consoled.
|
||||
|
||||
## Opening a session
|
||||
|
||||
From a server's page, choose **Console**. Then:
|
||||
|
||||
1. The UI calls `POST /api/console/connect`, which mints a **one-time** session
|
||||
token.
|
||||
2. The browser opens a WebSocket to `GET /api/console/tunnel` with that token.
|
||||
3. The server marks the token consumed — atomically, so a second use cannot
|
||||
race — and proxies the connection to guacd.
|
||||
|
||||
## Credentials
|
||||
|
||||
### SSH
|
||||
|
||||
Authenticates with a private key stored in the [key library](./ssh-keys.md). The
|
||||
key must have its private half uploaded; a public-only key cannot open a
|
||||
session.
|
||||
|
||||
### RDP and VNC
|
||||
|
||||
You supply credentials when connecting. They are encrypted, **single-use**, and
|
||||
consumed when the tunnel opens. They are not retained for the next session.
|
||||
|
||||
:::info Why single-use
|
||||
A stored console credential is a standing grant to that machine for anyone who
|
||||
can reach the endpoint. Consuming it at tunnel-open means a leaked session token
|
||||
is worth one connection at most, and only until it is used.
|
||||
:::
|
||||
|
||||
## Session behaviour
|
||||
|
||||
Closing the tab ends the session. There is no reconnect and no session
|
||||
persistence — reopening mints a new token and a new connection.
|
||||
|
||||
## Auditing
|
||||
|
||||
Opening a console is an audited action, with actor, server and time. What
|
||||
happens *inside* the session is not recorded: there is no session capture or
|
||||
keystroke log. If you need that, it has to come from the target machine.
|
||||
|
||||
## When it does not work
|
||||
|
||||
| Symptom | Cause |
|
||||
| --- | --- |
|
||||
| Connects then closes immediately | guacd unreachable — check `GUACD_ADDR` and that the container is up |
|
||||
| SSH refuses the key | The stored key has no private half, or is not in the target's `authorized_keys` |
|
||||
| RDP fails on a fresh credential | Credentials are consumed on open; a retry needs them entered again |
|
||||
| Hangs at connecting | The control plane cannot reach the target on the protocol port |
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
id: monitors
|
||||
title: Monitors
|
||||
sidebar_label: Monitors
|
||||
---
|
||||
|
||||
Monitors check that something is answering. Four types, two places they can run
|
||||
from, and a notification path when they stop being satisfied.
|
||||
|
||||
## Types
|
||||
|
||||
| Type | Checks | Options |
|
||||
| --- | --- | --- |
|
||||
| `http` | An HTTP(S) URL | method, expected status, keyword that must appear in the body, allow insecure TLS |
|
||||
| `tcp` | A host and port accept a connection | — |
|
||||
| `icmp` | A host answers ping | — |
|
||||
| `tls` | A certificate is valid and not expiring | warn N days before expiry |
|
||||
|
||||
An `http` monitor with a keyword is usually the one you want for an application:
|
||||
a 200 that returns an error page still fails the keyword.
|
||||
|
||||
## Where a check runs
|
||||
|
||||
Every monitor has a **runner**:
|
||||
|
||||
| Runner | Meaning |
|
||||
| --- | --- |
|
||||
| `server` | The control plane's scheduler performs the check |
|
||||
| a server ID | That server's agent performs it locally and reports the result |
|
||||
|
||||
Use `server` for anything reachable from the control plane — public endpoints,
|
||||
your own front door. Use an agent for anything only reachable from inside the
|
||||
target network: a database on a private subnet, a service bound to localhost, a
|
||||
device on a management VLAN.
|
||||
|
||||
:::tip Agent-run monitors measure what your users can't
|
||||
A check run from the control plane tells you the service is reachable from
|
||||
there. A check run on the machine tells you the process is alive. Those are
|
||||
different questions, and outages usually live in the gap.
|
||||
:::
|
||||
|
||||
## Interval, retries and state
|
||||
|
||||
- **Interval** — how often to check.
|
||||
- **Retries** — how many consecutive failures are tolerated before the state
|
||||
flips.
|
||||
|
||||
A monitor sits in `pending` until its first result. Failures accumulate; once
|
||||
they exceed `retries`, the monitor goes `down`, an **incident** opens and the
|
||||
attached notification channels fire. A subsequent success closes the incident.
|
||||
|
||||
Retries are what keeps one dropped packet from paging you. Set them to at least
|
||||
`1` for anything crossing the public internet.
|
||||
|
||||
## Notifications
|
||||
|
||||
Attach one or more [notification channels](./notification-channels.md) to a
|
||||
monitor. Channels are shared, so one Slack destination can serve every monitor
|
||||
you have.
|
||||
|
||||
Notification state is tracked per monitor, so a service that is down for six
|
||||
hours does not send a message per interval.
|
||||
|
||||
## Uptime and incidents
|
||||
|
||||
The monitor detail page shows:
|
||||
|
||||
- **Uptime**, from hourly rollup records — checks performed, how many were up,
|
||||
and mean latency per hour. Rollups are what make the graph cheap to draw over
|
||||
long windows.
|
||||
- **Incidents**, each with a start, a resolution and the cause recorded at the
|
||||
moment it opened.
|
||||
|
||||
## Disabling versus deleting
|
||||
|
||||
Disabling stops the checks and keeps the history. Deleting removes the monitor.
|
||||
Prefer disabling for anything seasonal — the uptime record is usually the part
|
||||
you wanted.
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
id: notification-channels
|
||||
title: Notification channels
|
||||
sidebar_label: Notification channels
|
||||
---
|
||||
|
||||
A channel is a destination for alerts. [Monitors](./monitors.md) reference
|
||||
channels by ID, so one destination serves as many monitors as you like.
|
||||
|
||||
Manage them at **Settings → Notifications**.
|
||||
|
||||
## Types
|
||||
|
||||
### Webhook
|
||||
|
||||
Posts JSON to a URL you choose.
|
||||
|
||||
| Setting | |
|
||||
| --- | --- |
|
||||
| `url` | Required |
|
||||
|
||||
```json
|
||||
{
|
||||
"monitor": "API front door",
|
||||
"type": "http",
|
||||
"old_status": "up",
|
||||
"new_status": "down",
|
||||
"message": "HTTP 502",
|
||||
"time": "2026-07-28T09:14:02Z"
|
||||
}
|
||||
```
|
||||
|
||||
Any response of 300 or above counts as a delivery failure. The request times out
|
||||
after 10 seconds.
|
||||
|
||||
### Discord
|
||||
|
||||
| Setting | |
|
||||
| --- | --- |
|
||||
| `url` | Discord webhook URL |
|
||||
|
||||
Posts the alert as message content.
|
||||
|
||||
### Slack
|
||||
|
||||
| Setting | |
|
||||
| --- | --- |
|
||||
| `url` | Slack incoming webhook URL |
|
||||
|
||||
### Telegram
|
||||
|
||||
| Setting | |
|
||||
| --- | --- |
|
||||
| `token` | Bot token |
|
||||
| `chat_id` | Target chat |
|
||||
|
||||
### SMTP
|
||||
|
||||
| Setting | |
|
||||
| --- | --- |
|
||||
| `host`, `port` | Required |
|
||||
| `from`, `to` | Required |
|
||||
| `username`, `password` | Optional; auth is skipped when the username is empty |
|
||||
|
||||
Port `465` uses implicit TLS; anything else uses STARTTLS.
|
||||
|
||||
Alert emails are rendered by the same email system that sends licence and
|
||||
account mail, so a monitor alert and an account email look like the same
|
||||
product.
|
||||
|
||||
## The message
|
||||
|
||||
Non-webhook channels all send the same one-line title:
|
||||
|
||||
```
|
||||
[Vantage] API front door (http) is DOWN: HTTP 502
|
||||
```
|
||||
|
||||
Recoveries read `recovered` in place of `is DOWN`. The webhook payload carries
|
||||
the same information as fields, which is the one to use if you are routing into
|
||||
something that needs to branch on status.
|
||||
|
||||
## Testing
|
||||
|
||||
Every channel has a **Test** button. It dispatches a fabricated down event for a
|
||||
monitor called "Test monitor", through the real delivery path — so a test that
|
||||
arrives proves the credentials, the network path and the destination, not just
|
||||
the configuration form.
|
||||
|
||||
:::tip Test after every change
|
||||
Channel settings are only exercised when something breaks, which is the worst
|
||||
time to discover a stale webhook URL. Re-test after rotating a token.
|
||||
:::
|
||||
|
||||
## Choosing destinations
|
||||
|
||||
- Use a **chat channel** for awareness, and make sure someone owns it.
|
||||
- Use **SMTP** where a durable record matters.
|
||||
- Use a **webhook** to reach an on-call system that does escalation properly.
|
||||
Vantage does not do escalation, rotas or acknowledgement; a webhook into
|
||||
something that does is the intended answer.
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
id: secrets
|
||||
title: Secrets vault
|
||||
sidebar_label: Secrets
|
||||
---
|
||||
|
||||
Key/value pairs, grouped by name, encrypted at rest with AES-256-GCM under
|
||||
`KEY_ENCRYPTION_KEY`. Two things consume them: workflow steps, and Kubernetes
|
||||
External Secrets Operator.
|
||||
|
||||
## Groups and values
|
||||
|
||||
A **group** is a named bundle — `prod-db`, `registry`, `acme-api`. Inside it are
|
||||
key/value pairs.
|
||||
|
||||
Group by consumer, not by type. A group is the unit a workflow step references
|
||||
and the unit ESO reads, so a group that matches one consumer is one reference;
|
||||
a group holding everything is over-sharing to every step that needs any of it.
|
||||
|
||||
## Managing them
|
||||
|
||||
**Secrets → New group**, then add keys.
|
||||
|
||||
Values are write-then-hidden. The list shows keys, never values. **Reveal** is a
|
||||
separate action on a separate endpoint, and it writes an audit event — so
|
||||
looking at a secret is a recorded act.
|
||||
|
||||
Deleting a single key and deleting the whole group are separate operations.
|
||||
|
||||
## Using secrets in workflows
|
||||
|
||||
Add the group name to a step's `secret_refs`. At execution the group's pairs are
|
||||
injected into the step's environment:
|
||||
|
||||
```bash
|
||||
# secret_refs: ["registry"]
|
||||
echo "$REGISTRY_PASSWORD" | docker login registry.example.com -u "$REGISTRY_USER" --password-stdin
|
||||
```
|
||||
|
||||
A workflow can also override `secret_refs` per step, without changing the
|
||||
library entry.
|
||||
|
||||
:::warning A step can print its own secrets
|
||||
Injection puts values in the environment. If your script echoes them, or runs
|
||||
with `set -x`, they land in the run log — which is stored on disk and readable
|
||||
in the UI. Vantage does not scrub step output.
|
||||
:::
|
||||
|
||||
## Kubernetes External Secrets Operator
|
||||
|
||||
`GET /api/secrets/:group/values` returns a group's pairs for ESO, authenticated
|
||||
with a **bearer token** rather than a session.
|
||||
|
||||
1. Generate the token at **Settings → Integrations**. It is shown once; only its
|
||||
SHA-256 is stored.
|
||||
2. Put it in a Kubernetes secret.
|
||||
3. Point an ESO `SecretStore` at the endpoint with that bearer token.
|
||||
|
||||
The token is rotatable: generating a new one replaces the stored hash and
|
||||
invalidates the old one immediately.
|
||||
|
||||
:::danger This token reads every group
|
||||
It is instance-wide, not scoped to one group. Treat it as a credential to the
|
||||
whole vault: store it as a secret in the cluster, never in a manifest in git,
|
||||
and rotate it when anyone with access leaves.
|
||||
:::
|
||||
|
||||
## What the vault is not
|
||||
|
||||
- **Not a password manager.** There is no sharing, expiry or per-user
|
||||
visibility. Anyone who can sign in and reveal, can reveal.
|
||||
- **Not versioned.** Overwriting a value loses the previous one.
|
||||
- **Not recoverable without the key.** If `KEY_ENCRYPTION_KEY` is lost, so is
|
||||
every value. Back it up separately from the database.
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
id: servers
|
||||
title: Servers
|
||||
sidebar_label: Servers
|
||||
---
|
||||
|
||||
The fleet. Every managed machine runs an agent that connects outbound to the
|
||||
control plane, and everything else in Vantage — keys, workflows, monitors,
|
||||
consoles — targets these records.
|
||||
|
||||
## Enrolling a server
|
||||
|
||||
Covered step by step in [Add your first server](../getting-started/first-server.md).
|
||||
In short: **Servers → Add server** issues a single-use, one-hour token and shows
|
||||
a one-liner to run as root on the target machine.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `pending` | Enrolment created; the agent has not registered yet |
|
||||
| `active` | The agent registered and is syncing |
|
||||
| `offline` | Last-seen passed the threshold |
|
||||
|
||||
The offline sweep runs every two minutes, so a machine that has just gone away
|
||||
takes a little while to be marked as such. That delay is intentional — a single
|
||||
missed poll is not an outage.
|
||||
|
||||
## The server detail page
|
||||
|
||||
### Keys
|
||||
|
||||
Which SSH keys are assigned to this machine, and their state. See
|
||||
[SSH keys](./ssh-keys.md).
|
||||
|
||||
### Inventory
|
||||
|
||||
Agents report:
|
||||
|
||||
| Data | Refreshed |
|
||||
| --- | --- |
|
||||
| CPU, memory, swap, load | every 30 seconds |
|
||||
| Partitions, kernel, full static snapshot | every 15 minutes |
|
||||
|
||||
The two carry separate timestamps, so a stale static snapshot beside fresh
|
||||
metrics is normal rather than a fault.
|
||||
|
||||
### OS updates
|
||||
|
||||
Agents check for pending package updates hourly and report the count. From the
|
||||
server page you can:
|
||||
|
||||
- **Apply updates** — pushes `ApplyUpdatesCmd` down the command stream. The
|
||||
agent runs the platform's package manager and reports back.
|
||||
- **Update agent** — pushes `UpdateAgentCmd` with a target version; the agent
|
||||
downloads the release, verifies it and replaces itself. See
|
||||
[Agent updates](../operations/agent-updates.md).
|
||||
|
||||
:::warning Applying updates is not scheduled or staged
|
||||
It runs now, on that machine. If you need ordering, health gates or a canary,
|
||||
build it as a [workflow](./workflows.md) instead — that is what workflows exist
|
||||
for.
|
||||
:::
|
||||
|
||||
### Console
|
||||
|
||||
Opens a browser SSH, RDP or VNC session. See [Browser console](./browser-console.md).
|
||||
|
||||
## Windows servers
|
||||
|
||||
Windows agents register, heartbeat, run workflow steps and report inventory.
|
||||
They do not manage `authorized_keys` — the poll loop stops after the heartbeat
|
||||
on any non-Linux host. This is a deliberate scope decision, not a gap being
|
||||
worked on.
|
||||
|
||||
## Removing a server
|
||||
|
||||
Deleting the server record removes it from the fleet. It does **not** uninstall
|
||||
the agent, which will keep trying to sync and failing. Uninstall it on the
|
||||
machine too:
|
||||
|
||||
```bash
|
||||
systemctl disable --now vantage-agent
|
||||
rm -f /usr/local/bin/vantage-agent /etc/systemd/system/vantage-agent.service
|
||||
rm -rf /etc/vantage
|
||||
systemctl daemon-reload
|
||||
```
|
||||
|
||||
Keys previously written to `authorized_keys` stay on disk, because the agent is
|
||||
no longer running to remove them. Revoke and let the agent apply the change
|
||||
**before** you delete the server if that matters to you.
|
||||
|
||||
## Agent tokens
|
||||
|
||||
Each server has its own token. The control plane stores only its SHA-256; the
|
||||
plaintext exists in the agent's `0600` config and nowhere else. There is no way
|
||||
to read a token back out of the control plane — if one is lost, re-enrol the
|
||||
machine.
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
id: settings
|
||||
title: Settings
|
||||
sidebar_label: Settings
|
||||
---
|
||||
|
||||
One page, three groups: **Access**, **Monitoring** and **Integrations**. Plus
|
||||
the licence, which has its own page.
|
||||
|
||||
Settings require the `owner` or `admin` role.
|
||||
|
||||
:::info Where instance settings went
|
||||
Members and single sign-on used to live at `/settings/instance`. They are now
|
||||
the Access group at the top of this page — splitting "who can sign in" from "how
|
||||
this instance behaves" produced two half-pages and a nav entry nobody could
|
||||
distinguish from Settings. The old path still redirects.
|
||||
:::
|
||||
|
||||
## Access
|
||||
|
||||
### Members
|
||||
|
||||
Add, remove and re-role the people who can sign in.
|
||||
|
||||
| Role | Can |
|
||||
| --- | --- |
|
||||
| `owner` | Everything |
|
||||
| `admin` | Everything except owner-only settings |
|
||||
| `member` | Servers, keys, workflows, monitors, secrets, console |
|
||||
|
||||
Local members authenticate with email and a bcrypt-hashed password.
|
||||
|
||||
#### Members managed by Vantage HQ
|
||||
|
||||
On a cloud instance, people granted access from the HQ portal appear here as
|
||||
read-only rows with a link to the portal.
|
||||
|
||||
:::warning HQ-managed users cannot be edited locally
|
||||
Changing the role of, or deleting, an `hq`-sourced user is refused with `409`.
|
||||
HQ owns their role, their password and whether they exist at all — a local
|
||||
change would be overwritten by the next sync and would leave two writers for one
|
||||
password hash. Manage them from [People and roles](../hq/people-and-roles.md).
|
||||
:::
|
||||
|
||||
### Single sign-on (OIDC)
|
||||
|
||||
Configured per organisation:
|
||||
|
||||
| Field | |
|
||||
| --- | --- |
|
||||
| Issuer | Your provider's issuer URL |
|
||||
| Client ID | |
|
||||
| Client secret | Stored AES-256-GCM encrypted |
|
||||
|
||||
Sign-in then goes `/auth/oidc/start` → your provider → `/auth/oidc/callback`.
|
||||
|
||||
Local and OIDC users coexist. Keep at least one local owner: if the provider is
|
||||
misconfigured or unreachable, a local account is the way back in.
|
||||
|
||||
## Monitoring
|
||||
|
||||
- **Alert defaults** for monitors.
|
||||
- **Notification channels** — their own page. See
|
||||
[Notification channels](./notification-channels.md).
|
||||
|
||||
## Integrations
|
||||
|
||||
### Workflow log retention
|
||||
|
||||
How long run logs are kept.
|
||||
|
||||
| Value | Meaning |
|
||||
| --- | --- |
|
||||
| unset | 30 days |
|
||||
| a number | that many days |
|
||||
| `0` | forever |
|
||||
|
||||
### ESO read token
|
||||
|
||||
The bearer token External Secrets Operator uses to read secret groups. Shown
|
||||
once, stored as a SHA-256 hash, rotatable. See
|
||||
[Secrets](./secrets.md#kubernetes-external-secrets-operator).
|
||||
|
||||
## Licence
|
||||
|
||||
`/settings/license` shows the deployment, tier, server allowance, enabled
|
||||
features and expiry.
|
||||
|
||||
On **self-hosted**, paste a licence here. This works even while the current
|
||||
licence is expired — that is the way out of degraded mode.
|
||||
|
||||
On **cloud**, there is no paste form. The endpoint answers `409 cloud_managed`,
|
||||
because a cloud licence is written by HQ directly. The page links to the portal
|
||||
instead.
|
||||
|
||||
See [Licensing and entitlements](../hq/licensing-and-entitlements.md).
|
||||
|
||||
## Sessions
|
||||
|
||||
Sessions are an opaque token in the `km_session` cookie, held in Redis with a
|
||||
24-hour TTL. There is no per-session management UI; restarting Redis signs
|
||||
everyone out and affects nothing else.
|
||||
|
||||
## Host and organisation guard
|
||||
|
||||
On a multi-tenant deployment, a request to `<slug>.vantage.<tld>` resolves the
|
||||
organisation from the slug and rejects a session belonging to a different one.
|
||||
The label it looks for comes from `APP_ROOT_LABEL`.
|
||||
|
||||
:::warning A wrong `APP_ROOT_LABEL` disables the guard
|
||||
It does not fail loudly — it simply stops matching, and the host check stops
|
||||
protecting anything. If you serve the UI on a custom domain, set it to match.
|
||||
:::
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
id: ssh-keys
|
||||
title: SSH keys
|
||||
sidebar_label: SSH keys
|
||||
---
|
||||
|
||||
Vantage holds a library of public keys and decides, per server, which ones
|
||||
belong in `/root/.ssh/authorized_keys`. The agent makes the file match.
|
||||
|
||||
:::info root only
|
||||
Vantage manages `/root/.ssh/authorized_keys` and nothing else. There is no
|
||||
per-user key management. The agent runs as root because writing that file
|
||||
requires it.
|
||||
:::
|
||||
|
||||
## Adding a key
|
||||
|
||||
### Upload one you already have
|
||||
|
||||
**Keys → Add key**, paste the public half. Vantage stores the public key and its
|
||||
fingerprint, and never needs the private half for this path.
|
||||
|
||||
### Generate one on a server
|
||||
|
||||
Vantage can have an agent generate a keypair on a managed machine
|
||||
(`GenerateKeyCmd` over the command stream). The public half comes back to the
|
||||
library. You may optionally upload the private half too, in which case it is
|
||||
stored **AES-256-GCM encrypted** under `KEY_ENCRYPTION_KEY`.
|
||||
|
||||
The JSON representation of a key exposes only `has_private_key` and
|
||||
`has_passphrase` — never the material. Retrieving a stored private key is its
|
||||
own endpoint and its own audit event.
|
||||
|
||||
:::tip Why store a private key at all
|
||||
The [browser console](./browser-console.md) needs one to open an SSH session. If
|
||||
you are not using the console, do not upload private halves.
|
||||
:::
|
||||
|
||||
## Assigning
|
||||
|
||||
Assign a key to one or more servers. Within one poll interval — 30 seconds — the
|
||||
agent picks up the change.
|
||||
|
||||
## Revoking
|
||||
|
||||
Revocation is **soft**: the assignment gets a `revoked_at` timestamp rather than
|
||||
being deleted, so the history of who had access to what, and when, survives.
|
||||
|
||||
The agent treats a revoked assignment as "not desired" and removes the line from
|
||||
`authorized_keys` on its next sync.
|
||||
|
||||
:::warning Revoking does not close open sessions
|
||||
It removes the key from the file. An SSH session already established stays up
|
||||
until it ends. Kill sessions on the machine if that matters.
|
||||
:::
|
||||
|
||||
## What the agent actually does
|
||||
|
||||
Each poll:
|
||||
|
||||
1. `SyncKeys` returns the desired set of public keys for that server.
|
||||
2. The agent reads `/root/.ssh/authorized_keys` and computes fingerprints.
|
||||
3. **If the sets match, it writes nothing.** No disk churn on unchanged state,
|
||||
which is most polls.
|
||||
4. If they differ, it writes a temporary file, then `os.Rename()`s it over the
|
||||
real one and sets mode `0600`.
|
||||
|
||||
The rename is atomic, so a machine that dies mid-write keeps the old file
|
||||
intact. There is no window in which `authorized_keys` is truncated or partial.
|
||||
|
||||
:::danger Vantage owns the whole file
|
||||
The agent rewrites `authorized_keys` to match the desired set. Keys added by
|
||||
hand on the machine are removed on the next change. If a key must survive, put
|
||||
it in Vantage.
|
||||
:::
|
||||
|
||||
## Recovering from a lockout
|
||||
|
||||
If you have removed every key from a machine and cannot get in, you still have
|
||||
the console — provided a private key is stored — or out-of-band access from your
|
||||
hosting provider. Vantage has no backdoor and does not keep a break-glass key.
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
id: workflows
|
||||
title: Workflows and steps
|
||||
sidebar_label: Workflows
|
||||
---
|
||||
|
||||
A **step** is a reusable script with declared inputs, outputs and secret
|
||||
references. A **workflow** composes steps in order and targets a set of servers.
|
||||
Running one dispatches the steps to each target's agent and streams the output
|
||||
back live.
|
||||
|
||||
## Steps
|
||||
|
||||
A step has:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `name`, `description` | Library identity |
|
||||
| `interpreter` | `bash` or `powershell` |
|
||||
| `script` | The body |
|
||||
| `declared_inputs` | Named parameters with defaults and descriptions |
|
||||
| `declared_outputs` | Names this step promises to export |
|
||||
| `secret_refs` | Vault entries injected as environment variables |
|
||||
|
||||
### Passing values between steps
|
||||
|
||||
Each step runs with `WORKFLOW_ENV` set to a file path. Anything written there as
|
||||
`KEY=value` becomes an environment variable for the **later steps of the same
|
||||
run on the same server**.
|
||||
|
||||
```bash
|
||||
HOSTNAME=$(hostname)
|
||||
echo "$HOSTNAME"
|
||||
echo "HOSTNAME=$HOSTNAME" >> $WORKFLOW_ENV
|
||||
```
|
||||
|
||||
That is the whole mechanism. `declared_outputs` documents what a step exports so
|
||||
the designer can show it; the file is what actually carries the value.
|
||||
|
||||
### Secrets
|
||||
|
||||
List a vault group in `secret_refs` and its key/value pairs are injected as
|
||||
environment variables when the step runs. They are not written to the run log
|
||||
unless your own script echoes them. See [Secrets](./secrets.md).
|
||||
|
||||
### The workspace
|
||||
|
||||
Every run gets a per-run working directory on each target. Steps share it, so
|
||||
one step can leave a file for the next. The agent removes it at the end of the
|
||||
run (`CleanupWorkspaceCmd`).
|
||||
|
||||
Do not use it for anything that must outlive the run.
|
||||
|
||||
## Default steps
|
||||
|
||||
A small library is seeded into every organisation at boot from the image, so a
|
||||
new install is not staring at an empty page.
|
||||
|
||||
:::warning Default steps are read-only
|
||||
Editing or deleting one is refused with `409`. Seeding rewrites them on every
|
||||
boot, so an edit would silently revert and a delete would come back at the next
|
||||
restart — refusing is the honest answer.
|
||||
|
||||
To customise one, use the per-step **script override** in the workflow designer,
|
||||
which belongs to that workflow and is not touched by seeding. To add to the
|
||||
shared library permanently, a file has to be committed to the repository and the
|
||||
server image rebuilt.
|
||||
:::
|
||||
|
||||
The UI mirrors this — the step modal opens read-only and Delete is hidden — but
|
||||
the API is the boundary; the UI is the courtesy.
|
||||
|
||||
## Building a workflow
|
||||
|
||||
1. **Workflows → New**.
|
||||
2. Add steps in order from the library.
|
||||
3. Set inputs per step.
|
||||
4. Set failure behaviour per step.
|
||||
5. Choose target servers.
|
||||
|
||||
### Failure behaviour
|
||||
|
||||
| `on_failure` | Effect |
|
||||
| --- | --- |
|
||||
| `stop` | Abort this server's run. Other servers continue |
|
||||
| `continue` | Record the failure, run the next step anyway |
|
||||
| `retry` | Re-run the step up to `max_retries`, then treat it as a failure |
|
||||
|
||||
### Per-step overrides
|
||||
|
||||
A workflow can override a step's script or its secret references without
|
||||
touching the library entry. This is how you adapt a default step, and it is
|
||||
scoped to that workflow.
|
||||
|
||||
## Running
|
||||
|
||||
**Run** snapshots the resolved steps into the run record and dispatches
|
||||
`RunStepCmd` to each target's agent over the command stream — no waiting for the
|
||||
next poll.
|
||||
|
||||
:::info Runs freeze their steps
|
||||
The snapshot is why editing a step tomorrow never rewrites what happened today.
|
||||
A run shows the script that actually executed, not the current library version.
|
||||
:::
|
||||
|
||||
Targets run **in parallel**; steps within one server run **in order**.
|
||||
|
||||
## Watching a run
|
||||
|
||||
Step stdout and stderr stream back as chunks, are appended to a log file on the
|
||||
server, and the UI follows them live. Each step records status, attempts, exit
|
||||
code and its exported environment.
|
||||
|
||||
**Cancel** stops a run in progress. Steps already running on an agent finish;
|
||||
nothing further is dispatched.
|
||||
|
||||
## Log retention
|
||||
|
||||
Run logs are swept on a schedule set by `workflow_log_retention_days` in
|
||||
Settings:
|
||||
|
||||
| Value | Meaning |
|
||||
| --- | --- |
|
||||
| unset | 30 days |
|
||||
| a number | that many days |
|
||||
| `0` | keep forever |
|
||||
|
||||
## Import and export
|
||||
|
||||
Steps export to a JSON file (`vantage.step/v1`) and import back, which is how
|
||||
you move a step between instances or keep one in version control. There is also
|
||||
a parse endpoint that turns a pasted script into a draft step by reading its
|
||||
declared inputs and outputs.
|
||||
|
||||
## Practical notes
|
||||
|
||||
- A step is a script. It runs as root, on the target, with no sandbox. Review
|
||||
what you import.
|
||||
- Keep steps small and single-purpose; compose them in the workflow. That is
|
||||
what makes the library reusable rather than a folder of near-duplicates.
|
||||
- PowerShell steps only make sense on Windows targets and bash steps on Linux
|
||||
ones. Nothing stops you targeting the wrong one; the step simply fails.
|
||||
@@ -0,0 +1,123 @@
|
||||
import type * as Preset from "@docusaurus/preset-classic";
|
||||
import type { Config } from "@docusaurus/types";
|
||||
import { themes as prismThemes } from "prism-react-renderer";
|
||||
|
||||
// Served as a path on the marketing host (vantage.hostxtra.co.uk/docs), routed
|
||||
// by its own Nginx Proxy Manager location rather than by site/. A path and not
|
||||
// a subdomain on purpose: *.vantage.hostxtra.co.uk is the per-tenant instance
|
||||
// namespace, and APP_ROOT_LABEL resolves an org from the label before
|
||||
// "vantage", so a docs. label there would be read as a tenant slug.
|
||||
//
|
||||
// DOCS_BASE_URL has to agree with three things at once: the NPM location, the
|
||||
// directory the runtime image copies the build into, and this value. When they
|
||||
// disagree the HTML still loads and every asset 404s.
|
||||
const url = process.env.DOCS_URL || "https://vantage.hostxtra.co.uk";
|
||||
const baseUrl = process.env.DOCS_BASE_URL || "/docs/";
|
||||
const appUrl = process.env.APP_URL || "https://vantage.hostxtra.co.uk";
|
||||
const hqUrl = process.env.HQ_URL || "https://vantage-hq.hostxtra.co.uk";
|
||||
|
||||
const config: Config = {
|
||||
title: "Vantage Docs",
|
||||
tagline: "Fleet management for servers you actually own",
|
||||
favicon: "img/favicon.svg",
|
||||
|
||||
url,
|
||||
baseUrl,
|
||||
|
||||
organizationName: "hostxtra",
|
||||
projectName: "vantage",
|
||||
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenAnchors: "throw",
|
||||
|
||||
i18n: { defaultLocale: "en", locales: ["en"] },
|
||||
|
||||
markdown: {
|
||||
mermaid: true,
|
||||
hooks: { onBrokenMarkdownLinks: "throw" },
|
||||
},
|
||||
themes: [
|
||||
"@docusaurus/theme-mermaid",
|
||||
[
|
||||
// Compile-time index served from this origin. No Algolia account,
|
||||
// no external host, nothing to key or rotate.
|
||||
"@easyops-cn/docusaurus-search-local",
|
||||
{
|
||||
hashed: true,
|
||||
indexBlog: false,
|
||||
docsRouteBasePath: "/",
|
||||
highlightSearchTermsOnTargetPage: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
presets: [
|
||||
[
|
||||
"classic",
|
||||
{
|
||||
docs: {
|
||||
// Docs-only mode: the documentation is the site.
|
||||
routeBasePath: "/",
|
||||
sidebarPath: "./sidebars.ts",
|
||||
},
|
||||
blog: false,
|
||||
theme: { customCss: "./src/css/custom.css" },
|
||||
} satisfies Preset.Options,
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
colorMode: {
|
||||
// Light default, matching site/ and adminsite/. web/ is the only
|
||||
// app locked to dark, and that contrast is deliberate.
|
||||
defaultMode: "light",
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
navbar: {
|
||||
// Wordmark only. The logo mark is drawn with currentColor in
|
||||
// site/components/Logo.tsx, which an <img> src cannot inherit, and
|
||||
// baking a fill into the file would mean a hex that stops tracking
|
||||
// the theme.
|
||||
title: "Vantage",
|
||||
items: [
|
||||
{
|
||||
type: "docSidebar",
|
||||
sidebarId: "docs",
|
||||
position: "left",
|
||||
label: "Documentation",
|
||||
},
|
||||
{ href: appUrl, label: "Control plane", position: "right" },
|
||||
{ href: hqUrl, label: "Vantage HQ", position: "right" },
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: "light",
|
||||
links: [
|
||||
{
|
||||
title: "Documentation",
|
||||
items: [
|
||||
{ label: "Getting started", to: "/getting-started/what-is-vantage" },
|
||||
{ label: "Vantage", to: "/vantage/servers" },
|
||||
{ label: "Vantage HQ", to: "/hq/accounts-and-signup" },
|
||||
{ label: "Reference", to: "/reference/environment-variables" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Product",
|
||||
items: [
|
||||
{ label: "Control plane", href: appUrl },
|
||||
{ label: "Vantage HQ", href: hqUrl },
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `© ${new Date().getFullYear()} HostXtra.`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
additionalLanguages: ["bash", "powershell", "yaml", "json", "protobuf", "nginx"],
|
||||
},
|
||||
} satisfies Preset.ThemeConfig,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,32 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Hitting the container directly is a 403 on an empty document root
|
||||
# otherwise, which reads as a broken deploy rather than "wrong path".
|
||||
location = / {
|
||||
return 302 /docs/;
|
||||
}
|
||||
|
||||
# Hashed assets are immutable — the filename changes when the content does.
|
||||
location /docs/assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /docs/ {
|
||||
# Docusaurus emits a real page per route, so a miss is a genuine 404
|
||||
# rather than something a SPA fallback should paper over.
|
||||
try_files $uri $uri/ $uri.html /docs/404.html;
|
||||
}
|
||||
|
||||
error_page 404 /docs/404.html;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
}
|
||||
Generated
+20082
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "vantage-docs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"serve": "docusaurus serve",
|
||||
"clear": "docusaurus clear",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.10.2",
|
||||
"@docusaurus/preset-classic": "^3.10.2",
|
||||
"@docusaurus/theme-mermaid": "^3.10.2",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.52.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.10.2",
|
||||
"@docusaurus/tsconfig": "^3.10.2",
|
||||
"@docusaurus/types": "^3.10.2",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 3 chrome version",
|
||||
"last 3 firefox version",
|
||||
"last 5 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
|
||||
|
||||
// Authored by hand rather than autogenerated, so ordering is a decision and
|
||||
// not a filename accident.
|
||||
const sidebars: SidebarsConfig = {
|
||||
docs: [
|
||||
"index",
|
||||
{
|
||||
type: "category",
|
||||
label: "Getting started",
|
||||
collapsed: false,
|
||||
items: [
|
||||
"getting-started/what-is-vantage",
|
||||
"getting-started/cloud-vs-self-hosted",
|
||||
"getting-started/self-hosted-install",
|
||||
"getting-started/first-login",
|
||||
"getting-started/first-server",
|
||||
"getting-started/claim-free-licence",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Vantage",
|
||||
items: [
|
||||
"vantage/servers",
|
||||
"vantage/ssh-keys",
|
||||
"vantage/workflows",
|
||||
"vantage/monitors",
|
||||
"vantage/notification-channels",
|
||||
"vantage/secrets",
|
||||
"vantage/browser-console",
|
||||
"vantage/audit-log",
|
||||
"vantage/settings",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Vantage HQ",
|
||||
items: [
|
||||
"hq/accounts-and-signup",
|
||||
"hq/people-and-roles",
|
||||
"hq/cloud-instances",
|
||||
"hq/self-hosted-instances",
|
||||
"hq/licensing-and-entitlements",
|
||||
"hq/billing",
|
||||
"hq/free-tier",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Reference",
|
||||
items: [
|
||||
"reference/environment-variables",
|
||||
"reference/rest-api",
|
||||
"reference/grpc-api",
|
||||
"reference/agent-config",
|
||||
"reference/ports-and-networking",
|
||||
"reference/troubleshooting",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Operations",
|
||||
items: [
|
||||
"operations/upgrading",
|
||||
"operations/backups",
|
||||
"operations/agent-updates",
|
||||
"operations/ci-cd",
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
@@ -0,0 +1,236 @@
|
||||
/* ==========================================================================
|
||||
Vantage documentation design tokens
|
||||
|
||||
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.
|
||||
Nothing enforces the match automatically: change a token in one file and you
|
||||
change it in all of them, in the same commit.
|
||||
|
||||
Everything below the token block maps Docusaurus's --ifm-* variables onto
|
||||
these. No rule in this file, and no component in this app, may carry a hex
|
||||
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
|
||||
writer.
|
||||
========================================================================== */
|
||||
|
||||
:root,
|
||||
:root[data-theme="light"] {
|
||||
--ground: #eaedf3;
|
||||
--panel: #ffffff;
|
||||
--panel-2: #f4f6fa;
|
||||
--ink: #0a1b33;
|
||||
--ink-2: #41556f;
|
||||
--ink-3: #6c7f96;
|
||||
--rule: #cdd6e2;
|
||||
--rule-soft: #e0e6ef;
|
||||
--accent: #0b2a58;
|
||||
--accent-ink: #ffffff;
|
||||
--up: #2f8a60;
|
||||
--down: #c6462f;
|
||||
--pend: #b0801f;
|
||||
--shadow: 0 1px 0 rgba(10, 27, 51, 0.05), 0 18px 40px -26px rgba(10, 27, 51, 0.45);
|
||||
--logo: #0b2a58;
|
||||
|
||||
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--mono: ui-monospace, "Cascadia Mono", "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
|
||||
--s--1: clamp(0.76rem, 0.74rem + 0.1vw, 0.81rem);
|
||||
--s-0: clamp(1rem, 0.97rem + 0.14vw, 1.05rem);
|
||||
--s-1: clamp(1.16rem, 1.09rem + 0.32vw, 1.36rem);
|
||||
--s-2: clamp(1.5rem, 1.34rem + 0.74vw, 2rem);
|
||||
--s-3: clamp(2rem, 1.66rem + 1.6vw, 3.1rem);
|
||||
--s-4: clamp(2.6rem, 1.9rem + 3.3vw, 4.9rem);
|
||||
|
||||
--rail: 1200px;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--ground: #071628;
|
||||
--panel: #0d2138;
|
||||
--panel-2: #102842;
|
||||
--ink: #e4ecf6;
|
||||
--ink-2: #9fb3ca;
|
||||
--ink-3: #71879f;
|
||||
--rule: #1e3855;
|
||||
--rule-soft: #172c44;
|
||||
--accent: #5b9be8;
|
||||
--accent-ink: #04101f;
|
||||
--up: #4fb484;
|
||||
--down: #e2705a;
|
||||
--pend: #d6a63f;
|
||||
--shadow: 0 1px 0 rgba(0, 0, 0, 0.35), 0 20px 44px -26px rgba(0, 0, 0, 0.85);
|
||||
--logo: #7fb2f0;
|
||||
}
|
||||
|
||||
/* ---------- Docusaurus mapping ---------- */
|
||||
|
||||
:root {
|
||||
--ifm-color-primary: var(--accent);
|
||||
--ifm-color-primary-dark: var(--accent);
|
||||
--ifm-color-primary-darker: var(--accent);
|
||||
--ifm-color-primary-darkest: var(--accent);
|
||||
--ifm-color-primary-light: var(--accent);
|
||||
--ifm-color-primary-lighter: var(--accent);
|
||||
--ifm-color-primary-lightest: var(--accent);
|
||||
|
||||
--ifm-background-color: var(--ground);
|
||||
--ifm-background-surface-color: var(--panel);
|
||||
|
||||
--ifm-font-family-base: var(--sans);
|
||||
--ifm-font-family-monospace: var(--mono);
|
||||
--ifm-font-size-base: var(--s-0);
|
||||
--ifm-line-height-base: 1.65;
|
||||
|
||||
--ifm-heading-font-weight: 800;
|
||||
--ifm-heading-line-height: 1.15;
|
||||
--ifm-h1-font-size: var(--s-3);
|
||||
--ifm-h2-font-size: var(--s-2);
|
||||
--ifm-h3-font-size: var(--s-1);
|
||||
|
||||
--ifm-font-color-base: var(--ink);
|
||||
--ifm-heading-color: var(--ink);
|
||||
--ifm-link-color: var(--accent);
|
||||
--ifm-link-hover-color: var(--accent);
|
||||
--ifm-toc-link-color: var(--ink-2);
|
||||
|
||||
--ifm-navbar-background-color: var(--panel);
|
||||
--ifm-navbar-shadow: none;
|
||||
--ifm-navbar-link-color: var(--ink-2);
|
||||
--ifm-navbar-link-hover-color: var(--ink);
|
||||
|
||||
--ifm-footer-background-color: var(--panel-2);
|
||||
--ifm-footer-color: var(--ink-2);
|
||||
--ifm-footer-link-color: var(--ink-2);
|
||||
--ifm-footer-title-color: var(--ink);
|
||||
|
||||
--ifm-menu-color: var(--ink-2);
|
||||
--ifm-menu-color-active: var(--accent);
|
||||
--ifm-menu-color-background-active: var(--panel-2);
|
||||
--ifm-menu-color-background-hover: var(--panel-2);
|
||||
|
||||
--ifm-toc-border-color: var(--rule-soft);
|
||||
--ifm-hr-border-color: var(--rule-soft);
|
||||
--ifm-table-border-color: var(--rule-soft);
|
||||
--ifm-table-stripe-background: var(--panel-2);
|
||||
--ifm-table-head-background: var(--panel-2);
|
||||
|
||||
--ifm-code-background: var(--panel-2);
|
||||
--ifm-code-font-size: 0.88em;
|
||||
--ifm-pre-background: var(--panel-2);
|
||||
--ifm-blockquote-color: var(--ink-2);
|
||||
--ifm-blockquote-border-color: var(--rule);
|
||||
|
||||
--ifm-global-radius: 4px;
|
||||
--ifm-alert-border-radius: 4px;
|
||||
--ifm-button-border-radius: 4px;
|
||||
--ifm-global-shadow-lw: var(--shadow);
|
||||
--ifm-global-shadow-md: var(--shadow);
|
||||
|
||||
--ifm-breadcrumb-color-active: var(--accent);
|
||||
--ifm-breadcrumb-item-background-active: var(--panel-2);
|
||||
|
||||
--docusaurus-highlighted-code-line-bg: var(--rule-soft);
|
||||
}
|
||||
|
||||
/* Admonitions carry the semantic three. They read by shape and label as well
|
||||
as colour, which is the rule everywhere else in the product too. */
|
||||
.theme-admonition-note,
|
||||
.theme-admonition-info {
|
||||
--ifm-alert-background-color: var(--panel-2);
|
||||
--ifm-alert-border-color: var(--rule);
|
||||
--ifm-alert-foreground-color: var(--ink);
|
||||
}
|
||||
|
||||
.theme-admonition-tip {
|
||||
--ifm-alert-background-color: var(--panel-2);
|
||||
--ifm-alert-border-color: var(--up);
|
||||
--ifm-alert-foreground-color: var(--ink);
|
||||
}
|
||||
|
||||
.theme-admonition-warning {
|
||||
--ifm-alert-background-color: var(--panel-2);
|
||||
--ifm-alert-border-color: var(--pend);
|
||||
--ifm-alert-foreground-color: var(--ink);
|
||||
}
|
||||
|
||||
.theme-admonition-danger {
|
||||
--ifm-alert-background-color: var(--panel-2);
|
||||
--ifm-alert-border-color: var(--down);
|
||||
--ifm-alert-foreground-color: var(--ink);
|
||||
}
|
||||
|
||||
/* ---------- small corrections ---------- */
|
||||
|
||||
.navbar {
|
||||
border-bottom: 1px solid var(--rule-soft);
|
||||
}
|
||||
|
||||
.navbar__title {
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--rule-soft);
|
||||
}
|
||||
|
||||
.markdown h1,
|
||||
.markdown h2,
|
||||
.markdown h3 {
|
||||
letter-spacing: -0.03em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
margin-top: 2.4rem;
|
||||
padding-top: 1.6rem;
|
||||
border-top: 1px solid var(--rule-soft);
|
||||
}
|
||||
|
||||
.markdown > p,
|
||||
.markdown li {
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.markdown strong {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu {
|
||||
font-size: var(--s--1);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.menu__list-item-collapsible .menu__link--sane,
|
||||
.menu__link {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* The reference tables in Reference/ are wide by nature; they scroll inside
|
||||
their own container rather than pushing the page sideways. */
|
||||
.markdown table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
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
|
||||
makes. Keep it in step with --logo.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="246 207 533 610">
|
||||
<g transform="translate(0,1024) scale(0.1,-0.1)" fill="#0b2a58" stroke="none">
|
||||
<path d="M4940 7767 c-96 -57 -528 -312 -960 -567 -678 -400 -1064 -628 -1187 -702 l-33 -20 0 -1357 0 -1357 293 -174 c160 -96 425 -252 587 -348 946 -559 1352 -799 1407 -833 34 -22 67 -39 72 -39 5 0 188 106 408 236 219 130 459 272 533 315 74 44 425 252 780 462 l645 382 0 1355 0 1355 -135 81 c-140 84 -1118 662 -1812 1070 -218 129 -402 236 -410 239 -7 3 -92 -42 -188 -98z m297 -331 c309 -182 971 -572 1208 -712 149 -88 372 -220 498 -294 l227 -135 0 -1175 0 -1175 -578 -342 c-317 -188 -694 -411 -837 -495 -143 -85 -344 -204 -447 -265 l-186 -111 -314 185 c-987 584 -1710 1013 -1725 1025 -10 8 -13 256 -13 1178 0 1099 1 1168 18 1182 9 8 150 93 312 188 162 96 417 246 565 333 805 476 1149 677 1156 677 4 0 56 -29 116 -64z" />
|
||||
<path d="M3760 6109 c0 -6 187 -396 417 -867 229 -471 483 -994 564 -1162 l148 -305 232 0 233 0 271 560 c150 308 403 830 564 1160 160 329 291 605 291 612 0 19 -553 19 -568 1 -9 -12 -229 -480 -652 -1390 -73 -158 -136 -285 -140 -283 -3 2 -100 206 -215 452 -115 246 -291 624 -390 838 l-182 390 -287 3 c-202 2 -286 -1 -286 -9z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
},
|
||||
"exclude": [".docusaurus", "build"]
|
||||
}
|
||||
Reference in New Issue
Block a user