Compare commits

..
10 Commits
Author SHA1 Message Date
mrhid6 f7eee4fc3e refactor: stand up vantage-docs as its own repository
Deploy / deploy (push) Successful in 2m15s
docsite/ becomes the repository root; the whole repository is the site.
The image is vantage/vantage-docs:latest, was mrhid6/vantage/docsite.

The workflow passes no build args, as the monorepo's did not: DOCS_URL,
DOCS_BASE_URL, APP_URL and HQ_URL have correct defaults in the
Dockerfile, and passing an unset repo variable would blank one rather
than leave it alone.
2026-09-08 08:35:49 +00:00
mrhid6 cca6453b86 refactor: move Vantage HQ out to the vantage-admin repository
admin/ and adminsite/ are extracted with their history to
gitea.hostxtra.co.uk/vantage/vantage-admin, where they are named server/
and web/ for what they are rather than for the services they run. Their
images move with them, to vantage/vantage-admin/{server,web}.

Nothing here imported them, so the cut is clean: the only coupling was
always at runtime, through admin writing into the control plane's
database. The parts of that contract this side enforces are unchanged and
still documented here — hq-sourced users, POST /license answering 409
cloud_managed, and FREE_INSTANCE_REAP_AFTER needing to match.

LICENSE_SIGNING_KEY now appears in no compose file in this repository.
Keeping it out used to be a rule someone had to remember; it is the
repository boundary now.

docker-compose.site.yml loses both services and gains a note on how the
host composes the three files together.
2026-09-08 08:13:55 +00:00
mrhid6 aa581b5d1e fix: Build the vantagectl image on its release tag, not on every push to main
vantagectl is a released tool, not a running service. An operator restoring a
database should be able to name the version they ran, and ":latest, rebuilt
whenever main moved" cannot be named after the fact.

The image now builds in vantagectl-release.yml on a vantagectl/v* tag, tagged
with that version as well as latest, with VERSION passed through so the binary
inside reports the tag rather than "dev". server-deploy.yml no longer builds
it and is back to seven images.

The cost is that a shared/ fix reaches the image only at the next release
rather than the next push to main. That is the intended trade and is written
down in CLAUDE.md next to the trigger table.
2026-09-07 15:11:39 +00:00
mrhid6 f41f2aaae7 feat: Add vantagectl, the control plane backup and restore CLI
Vantage encrypts SSH private keys, vault secrets, SSO client secrets and
console credentials with KEY_ENCRYPTION_KEY. That key is in no backup and is
not recoverable, so restoring a database without it produces a control plane
whose every secret is permanently unreadable.

vantagectl dumps and restores a whole database and stamps a SHA-256
fingerprint of the key into every archive, so a restore refuses rather than
producing that database. The key itself is never written.

- shared/cryptobox: AES-GCM extracted so the server and the CLI share one
  implementation rather than a copy that can drift
- shared/backup: manifest, tar+gzip archive with per-member checksums
  verified before the first write, dump, restore, verify
- vantagectl: its own module, so cobra stays out of the server, admin and
  sitesvc module graphs
- container image, tagged release workflow, CI rebuild trigger
- optional Helm CronJob, off by default
- operator documentation, reconciled with the existing backups page
2026-09-07 14:52:13 +00:00
mrhid6 76057eb346 fix: document inspect and --confirm-db's actual behaviour 2026-09-07 14:45:37 +00:00
mrhid6 f564dd0479 docs: Reconcile backups and backup-and-restore pages
backups.md kept its store-level table and danger note but now points to
vantagectl as the supported path, with mongodump/mongorestore demoted to
an explicit fallback and a warning that a plain dump records no key
fingerprint. backup-and-restore.md links back for the store-level
overview.
2026-09-07 14:32:31 +00:00
mrhid6 54671a3652 docs: Document backup and restore
The page leads with KEY_ENCRYPTION_KEY rather than mentioning it in a
note, because holding a good database dump and no key is the way this goes
wrong.
2026-09-07 14:29:03 +00:00
mrhid6 8359791b6e fix: Fixed api url on web 2026-08-25 13:53:59 +00:00
mrhid6 036875345a feat: sell status pages as a per-instance licence feature 2026-08-25 09:25:05 +00:00
mrhid6 c6cd96cbd1 docs: correct the status page URL for self-hosted, trim to what ships
- The status page URL was given only as `<instance>.vantage.<tld>`, which a
  self-hosted install does not serve. Both deployments are now described.
- The banner is documented as one notice: the editor exposes no level picker
  and the view renders every level identically.
- `pending` added to the component states, which a monitor with no result yet
  renders.
- Delete page documented alongside un-publish.
- `TRUSTED_PROXIES` names the LAN case: with the RFC1918 default, a client on a
  private range reaching the server directly is itself trusted and can spoof
  `X-Forwarded-For` — and now `X-Forwarded-Host`. Narrow it to the proxy.
- CLAUDE.md: scopes are nine resources, not eight; `status-pages` added to the
  REST route table; the host-resolution rules recorded under Status pages.
2026-08-25 09:05:06 +00:00
14 changed files with 487 additions and 20 deletions
+45
View File
@@ -0,0 +1,45 @@
name: Deploy
on:
push:
branches:
- main
# DOCS_URL, DOCS_BASE_URL and APP_URL are repo variables, and editing one
# pushes no commit — so nothing would rebuild on its own. This is the
# escape hatch for that, and DOCS_BASE_URL in particular is baked into
# every asset path in the build.
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-docker
container: docker:dind
steps:
- name: Setup
run: apk add --update nodejs npm git
- name: Checkout
uses: actions/checkout@v4
- name: Log in to registry
run: |
echo "${{ secrets.RELEASE_TOKEN }}" | \
docker login ${{ vars.DOCKER_HOST }} \
-u "${{ secrets.REGISTRY_USER }}" --password-stdin
# One image, one directory — the whole repository is the docs site,
# so there is nothing to detect a change in. It rebuilds on every
# push, which is what a docs site wants anyway.
#
# No build args are passed, deliberately: DOCS_URL, DOCS_BASE_URL,
# APP_URL and HQ_URL all have correct defaults in the Dockerfile,
# and passing an unset repo variable would blank one rather than
# leave it alone. DOCS_BASE_URL in particular must match the proxy
# location that routes here and the directory the runtime stage
# serves from; when those three disagree the HTML loads and every
# asset 404s. Change it in the Dockerfile, not here.
- name: Build and push image
run: |
IMAGE="${{ vars.DOCKER_HOST }}/vantage/vantage-docs:latest"
docker build -t "$IMAGE" .
docker push "$IMAGE"
+1 -1
View File
@@ -1,4 +1,4 @@
node_modules
build
.docusaurus
.cache-loader
.env
+90
View File
@@ -0,0 +1,90 @@
# Vantage documentation (`vantage-docs`)
The user-facing documentation, served at **vantage.hostxtra.co.uk/docs**.
Docusaurus 3 in docs-only mode (`routeBasePath: "/"`, no blog), one version
tracking `main`, search indexed at build time by
`@easyops-cn/docusaurus-search-local` so nothing external is keyed or called.
Extracted from the `vantage` monorepo with its history, where it was
`docsite/`. The whole repository is the site — there is no subdirectory.
It documents the **product**, not the codebase: each repository's own CLAUDE.md
remains the contributor's map, and the two are allowed to differ in altitude but
not in fact. Five sections — Getting started, Vantage, Vantage HQ, Reference,
Operations — with `sidebars.ts` authored by hand so ordering is a decision
rather than a filename accident.
## A path, not a subdomain
`*.vantage.hostxtra.co.uk` is the per-tenant instance namespace, and the control
plane's `APP_ROOT_LABEL` resolves an instance from the label before `vantage`
so a `docs.` label would be read as a tenant slug. Hence `/docs` on the
marketing host instead.
**Three things must agree, and when they do not the HTML loads while every
asset 404s:**
1. `DOCS_BASE_URL` (default `/docs/` in the Dockerfile)
2. the proxy location that routes here — which must also sort **above** the
catch-all forwarding to `site:3003`, or Next answers the 404 first
3. the directory the runtime stage serves from,
`/usr/share/nginx/html/docs`
The proxy forwards the **full** path; it does not strip `/docs`. That is the
whole reason the third item exists.
Unlike the three Next apps this builds to static files, so its runtime stage is
`nginx:alpine-slim` rather than Node, and it listens on **80**, not 3000.
## Design tokens
`src/css/custom.css` holds `vantage-site`'s `web/app/globals.css` token blocks
**copied verbatim** — same names, same values. This is the one place the tokens
are not consumed through Tailwind: everything below the token block maps
Docusaurus's `--ifm-*` variables onto them. Docusaurus already stamps
`data-theme` on `<html>`, which is the selector the dark block keys on, so the
built-in toggle needed no wiring.
The rule holds all the same: **no rule in that file outside the token blocks may
carry a hex.** The one concession is `static/img/favicon.svg`, which must, for
the same reason the email layout must — a browser tab cannot read a token.
**Nothing enforces the match across the four front ends, and they are now in
four repositories.** A token change here follows one in `vantage-site`; there is
no way to make them one commit.
## Deployment
A fragment, composed on top of the control plane's own file:
```bash
docker compose \
-f vantage/deploy/docker/docker-compose.yml \
-f vantage-site/deploy/docker-compose.yml \
-f vantage-docs/deploy/docker-compose.yml \
-f vantage-admin/deploy/docker-compose.yml \
up -d
```
Not part of a self-hosted install.
## CI/CD
`.gitea/workflows/deploy.yml`, on every push to `main` plus `workflow_dispatch`.
One image, `vantage/vantage-docs:latest`, from the repository root — the whole
repository is the site, so there is nothing to detect a change in and it
rebuilds on every push.
**No build args are passed, deliberately.** `DOCS_URL`, `DOCS_BASE_URL`,
`APP_URL` and `HQ_URL` all have correct defaults in the Dockerfile, and passing
an unset repo variable would blank one rather than leave it alone — which for
`DOCS_BASE_URL` means every asset path in the build. Change them in the
Dockerfile. The monorepo's workflow passed none either; this preserves that.
| Name | Type | Value |
| --------------- | ------ | ----------------------------------------------------------- |
| `RELEASE_TOKEN` | Secret | Gitea API token, `write:package`. No module fetch here, so it needs no org read access |
| `REGISTRY_USER` | Secret | Gitea username. Must own `RELEASE_TOKEN` |
| `DOCKER_HOST` | Variable | registry host used for image tags |
Despite the name the workflow **does not deploy**; it only builds and pushes.
+1 -1
View File
@@ -25,7 +25,7 @@ RUN npm run build
# Runtime stage
#
# Docusaurus emits a fully static site, so unlike web/, site/ and adminsite/
# Docusaurus emits a fully static site, so unlike web/ and site/
# there is no Node server at runtime. alpine-slim is roughly a quarter the size
# of caddy:alpine, and nothing here needs automatic TLS — the host proxy
# terminates it.
+24
View File
@@ -0,0 +1,24 @@
# The user documentation, served at vantage.hostxtra.co.uk/docs.
#
# A fragment, composed on top of the vantage repository's own file — see the
# vantage-site fragment for the full command.
#
# A path on the marketing host, not a subdomain: *.vantage.hostxtra.co.uk is
# the per-tenant instance namespace, and the control plane's APP_ROOT_LABEL
# would read a `docs.` label as a tenant slug.
#
# The proxy location that routes here must sort ABOVE the catch-all forwarding
# to site:3003, or Next answers the 404. It forwards the FULL path — it does
# not strip /docs — so the proxy location, DOCS_BASE_URL and the directory the
# image serves from (/usr/share/nginx/html/docs) must all agree. When they do
# not, the HTML loads and every asset 404s.
#
# Unlike the Next apps this is a static build behind nginx, so it listens on 80
# rather than 3000.
services:
docsite:
image: gitea.hostxtra.co.uk/vantage/vantage-docs:latest
restart: unless-stopped
ports:
- 3005:80
networks: {}
+10 -2
View File
@@ -96,8 +96,16 @@ rather than run in a half-prepared state.
## 4. Put a proxy in front
Point your reverse proxy at `web` on port `3000` and terminate TLS there. The
web app reaches the API internally, so there is no need to publish port `8080`.
Terminate TLS at your reverse proxy and route **one hostname to two backends**:
| Path | Backend |
| -------------------------------------------------------------------------------- | ------------- |
| `/api`, `/auth`, `/public`, `/install`, `/install.ps1`, `/update`, `/update.ps1` | `server:8080` |
| everything else | `web:3000` |
Both rules are required. The web app forwards nothing to the API, so a proxy
that sends the whole hostname to `web:3000` serves the interface and answers
`404` to every request it makes — starting with the login form.
Agents connect to port `9090`. Vantage does not terminate TLS itself, so put
that port behind your proxy too, with a certificate valid for the name in
+2 -1
View File
@@ -28,13 +28,14 @@ entitlement.
## Features
Three features are enabled per instance rather than bundled into a tier:
Four features are enabled per instance rather than bundled into a tier:
| Feature | What it enables |
| ---------------------- | ------------------------------------------------------------------------------- |
| Browser console | The [browser console](../vantage/browser-console.md) |
| Single sign-on | [Sign-in through your identity provider](../vantage/settings.md#single-sign-on) |
| Vulnerability scanning | [Package vulnerability scanning](../vantage/vulnerabilities.md) |
| Status pages | [Public status pages](../vantage/status-pages.md) |
No tier includes them by default; you enable them on the instances that need
them.
+233
View File
@@ -0,0 +1,233 @@
---
id: backup-and-restore
title: Backup and restore
sidebar_label: Backup and restore
---
`vantagectl` is a separate command-line tool that backs up and restores the
MongoDB database behind a Vantage control plane. It talks to MongoDB directly,
never to the Vantage API, so it works against a control plane that is down,
half-migrated, or gone — exactly the situation a backup tool has to survive.
For the store-level overview — what holds what, and why the database alone is
not a backup — see [Backups](./backups.md). This page covers the tool.
:::danger The key comes first
Vantage encrypts SSH private keys, key passphrases, vault secrets, SSO client
secrets and console credentials with `KEY_ENCRYPTION_KEY`. **It is not in your
backup, and it is not recoverable.** A database restored without it is
permanently unreadable — not degraded, not partially readable, unreadable.
Store it wherever you store the credentials you could not rebuild: a password
manager, a secrets vault outside this control plane, a piece of paper in a
safe. Anywhere but next to the archive.
:::
## What a backup holds
Every collection in the database, the index definitions each one needs to be
useful again, and a SHA-256 **fingerprint** of `KEY_ENCRYPTION_KEY` — never the
key itself. The fingerprint is what lets a later `restore` or `verify` tell you
that the key you are holding is the wrong one, before it writes a database
nobody can read.
## What it does not hold
- **Redis sessions.** Everyone signs in again after a restore, which is already
true whenever Redis itself restarts.
- **The vulnerability database.** It is re-pulled automatically on next boot.
- **Agent state on managed servers.** Nothing needs re-enrolling: agents
reconnect on their own, because `servers.agent_token_hash` — the thing an
agent authenticates with — is itself in the backup.
:::note Pin the version
The image is published on each `vantagectl/v*` release and tagged with that
version; `:latest` also moves. Pin a version in anything scheduled. A restore
is easier to reason about when you can say which build produced the archive and
which one read it back.
:::
## Taking a backup
The loose binary:
```bash
export MONGO_URI=mongodb://localhost:27017
export MONGO_DB=vantage
export KEY_ENCRYPTION_KEY=<your 64-char hex key>
vantagectl backup --out /backups
```
The container:
```bash
docker run --rm \
-e MONGO_URI=mongodb://mongo:27017 \
-e MONGO_DB=vantage \
-e KEY_ENCRYPTION_KEY=<your 64-char hex key> \
-v /backups:/backups \
gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:0.1.0 backup --out /backups
```
Kubernetes, as a scheduled `CronJob` the Helm chart can render for you:
```yaml
backup:
enabled: true
schedule: "0 2 * * *"
image: "gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:0.1.0"
pvcName: "vantage-backups"
```
`backup.enabled` defaults to `false`, and the chart refuses to render if it is
turned on without both `backup.image` and `backup.pvcName` — a backup needs a
known image and somewhere durable to land, and guessing at either is worse than
refusing to start. `backup.exclude` names collections to leave out (recorded in
the archive's manifest, so an archive never claims to be complete when it is
not), and `backup.successfulJobsHistoryLimit` / `backup.failedJobsHistoryLimit`
/ `backup.resources` behave exactly as they do on any other `CronJob`.
`backup` refuses to run without `KEY_ENCRYPTION_KEY` set in the environment,
unless you pass `--allow-no-key` — for a deployment that genuinely stores no
encrypted data. Everywhere else, treat the refusal as the tool doing its job.
## Where to put the archive
`--out -` streams the tarball to stdout instead of writing a file, and every
line of progress output goes to stderr — so piping the archive into something
else is always safe, nothing progress-related lands in the stream.
Into `restic`:
```bash
vantagectl backup --out - | restic backup --stdin --stdin-filename vantage.tar.gz
```
Into S3:
```bash
vantagectl backup --out - | aws s3 cp - s3://my-backups/vantage-$(date +%F).tar.gz
```
An archive is as sensitive as a raw database dump — it carries every SSH key
assignment, every secret group, every session-adjacent setting, in a form the
right `KEY_ENCRYPTION_KEY` can decrypt. Whatever you pipe it into should
encrypt it at rest; `vantagectl` itself does not.
## Checking a backup is real
```bash
vantagectl verify /backups/vantage-backup-vantage-20260907T020000Z.tar.gz \
--mongo-uri mongodb://localhost:27017 --db vantage
```
Each line of output answers a different question:
- **`Archive`** — every member's checksum still matches; the tarball has not
been truncated or corrupted.
- **`Archive key`** / **`Your key`** — the fingerprint stored in the archive
next to the fingerprint of the `KEY_ENCRYPTION_KEY` in your environment.
- **`Key match`** — whether those two fingerprints agree.
- **`Live probe`** — given `--mongo-uri`, `verify` goes one step further and
decrypts a real ciphertext value from that database with the key you hold.
A fingerprint match proves two archives agree about a key; only the probe
proves the key in your hand actually reads the data.
`verify` exits non-zero the moment anything above is wrong, which is what makes
it worth putting on a schedule — a backup job that "succeeded" last night is
not the same claim as a backup that will actually restore.
## Looking inside an archive
`inspect` prints an archive's manifest and touches no database at all — no
`--mongo-uri`, no key. It is what to run against an archive of unknown origin,
before deciding whether it is the one you want:
```bash
vantagectl inspect /backups/vantage-backup-vantage-20260907T020000Z.tar.gz
```
It reports when the archive was taken and on which host, the Vantage and
MongoDB versions behind it, the database it came from, the key fingerprint (or
that it carries none), every collection with its document count and size, and
anything `--exclude` left out. Opening the archive verifies every member's
checksum on the way, so a corrupt archive fails here too.
Reach for `verify` instead when the question is whether the key you hold opens
it; reach for `inspect` when the question is what it is.
## Restoring
`restore` expects the target database to be empty. Pointed at one that already
holds data, it refuses outright: there are no merge semantics, because merging
two control planes reconciles nothing and upserting old data over new would
resurrect revoked keys and deleted users.
```bash
vantagectl restore /backups/vantage-backup-vantage-20260907T020000Z.tar.gz \
--mongo-uri mongodb://localhost:27017 --db vantage_restore
```
To overwrite a database that is not empty, add `--force`, which drops each
collection named in the archive before loading it. `--force` always needs a
second assurance, in one of two forms:
- `--confirm-db NAME`, naming the target exactly. A mismatch is refused. This
works everywhere — on a terminal and in a Kubernetes Job, a CI step or a cron
entry alike — and is the form to script.
- Nothing, on a terminal: `--force` alone prompts you to type the target
database's name back, a deliberate pause before something destructive.
Without a terminal and without `--confirm-db`, `--force` is refused: there is
nobody there to prompt. Naming the database in the command itself means a
copy-pasted invocation carries its intended target with it and cannot destroy a
different one by accident.
`--force` drops only the collections the archive carries. Anything else already
in the target is left alone and named in a warning, so an archive taken with
`--exclude workflow_log_lines` restored over a live database tells you the old
log lines are still there, joined to freshly restored runs. Dropping them
instead would delete data you never asked to delete.
`restore` also refuses when the archive's key fingerprint does not match the
`KEY_ENCRYPTION_KEY` in your environment — see "When the key is wrong" below.
## The restore drill
An untested backup is a hypothesis, not a backup. Rehearse the whole path,
monthly:
1. Restore last night's archive into a scratch database:
```bash
vantagectl restore /backups/vantage-backup-vantage-<date>.tar.gz \
--mongo-uri mongodb://localhost:27017 --db vantage_drill
```
2. Run `verify` against the result to confirm the data that landed is actually
readable with your current key:
```bash
vantagectl verify /backups/vantage-backup-vantage-<date>.tar.gz \
--mongo-uri mongodb://localhost:27017 --db vantage_drill
```
3. Drop the scratch database. It served its purpose.
The failure this catches is not "the archive is corrupt" — `verify` alone
catches that. It is "the archive is fine but nobody can actually stand a
control plane back up from it," which only a real restore proves.
## When the key is wrong
If `restore` finds the archive's key fingerprint does not match the
`KEY_ENCRYPTION_KEY` you are running with, it stops. Passing
`--ignore-key-mismatch` proceeds anyway, but says plainly which collections
will come back with ciphertext nobody can read:
- `keys` — SSH private keys and passphrases
- `secrets` — the vault
- `auth_providers` — OIDC/SSO client secrets
- `console_sessions` — RDP/VNC credentials
There is no way to recover that ciphertext afterwards. If you have reached
this point, the right key was lost along with the chance to read those rows —
the fix is to re-enter each of them by hand (re-upload SSH keys, re-save vault
secrets, reconfigure SSO), not to keep searching for a way to decrypt what is
already in the database.
+27 -3
View File
@@ -24,7 +24,23 @@ values is permanently unreadable.
Store the key somewhere other than the server it protects.
:::
## Backing up MongoDB
:::info Use `vantagectl`
[**Backup and restore**](./backup-and-restore.md) is the supported way to take
and restore a backup. It writes an archive that carries a fingerprint of
`KEY_ENCRYPTION_KEY` — never the key — so a restore taken with the wrong key
**refuses** rather than silently producing a database whose secrets nobody can
read. It also checksums every archive member before writing anything, and
refuses to restore into a database that already holds data. A plain
`mongodump` does none of that: it records nothing about which key the data was
encrypted under, so a restore from one succeeds even when the key is wrong and
the failure only shows up later, as unreadable secrets.
The rest of this page, past the table above, describes the `mongodump` /
`mongorestore` fallback for an operator who does not have `vantagectl`
available. Prefer the linked page.
:::
## Backing up MongoDB (fallback, without `vantagectl`)
With the bundled Mongo container:
@@ -33,6 +49,13 @@ docker compose exec -T mongo mongodump --archive --gzip --db vantage \
> /backups/vantage-$(date +%F).archive.gz
```
:::warning
This archive records nothing about which `KEY_ENCRYPTION_KEY` it was taken
under. Restoring it with the wrong key produces a database that looks intact
and is not — every secret in it is silently unreadable until something tries
to decrypt one.
:::
Restoring:
```bash
@@ -69,12 +92,13 @@ What it does **not** do is reconcile the world. After a restore:
| What | When |
| ----------------- | ----------------------------------------------------- |
| MongoDB dump | Nightly, retained per your policy |
| Backup | Nightly, retained per your policy |
| Environment file | On change, held in a password manager or secret store |
| Restore rehearsal | Occasionally, into a throwaway host |
Rehearse a restore now and again. It is the step most often skipped, and the one
that finds the problems.
that finds the problems. See [Backup and restore](./backup-and-restore.md) for
the drill, and for `verify`, which checks a backup is real without a restore.
## Cloud instances
+1 -1
View File
@@ -25,7 +25,7 @@ it is absent.
| `VANTAGE_LICENSE` | no | | A licence supplied at startup, so an automated install does not have to paste one in |
| `VANTAGE_TRIVY_DB_REF` | no | `ghcr.io/aquasecurity/trivy-db:2` | Where the vulnerability database is pulled from. Point it at a mirror for an air-gapped install |
| `VANTAGE_VULNDB_DISABLED` | no | | `true` switches [vulnerability scanning](../vantage/vulnerabilities.md) off entirely. Findings already stored are still served, and still shown as stale |
| `TRUSTED_PROXIES` | no | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | Comma-separated CIDRs or addresses of proxies allowed to set `X-Forwarded-For`. The shipped Docker Compose and Helm chart default to the private RFC1918 ranges, which covers Nginx Proxy Manager on the Docker bridge network and Traefik on a Kubernetes pod CIDR. An operator whose proxy sits on a public address must set this themselves, or every visitor behind it shares one address for rate-limiting purposes. Unset entirely (outside those shipped defaults) trusts none, so the client address is the direct peer |
| `TRUSTED_PROXIES` | no | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | Comma-separated CIDRs or addresses of proxies allowed to set `X-Forwarded-For`. The shipped Docker Compose and Helm chart default to the private RFC1918 ranges, which covers Nginx Proxy Manager on the Docker bridge network and Traefik on a Kubernetes pod CIDR. An operator whose proxy sits on a public address must set this themselves, or every visitor behind it shares one address for rate-limiting purposes. Unset entirely (outside those shipped defaults) trusts none, so the client address is the direct peer. **On a LAN-only install, narrow this to your proxy's address.** The RFC1918 default trusts every private range, so a client on 192.168.0.0/16 reaching the server directly is itself a "trusted proxy" and can put whatever it likes in `X-Forwarded-For` — and, on the public status route, in `X-Forwarded-Host`. Behind a proxy on a public address, or with no proxy at all, that is not reachable; on a flat LAN it is |
:::danger `KEY_ENCRYPTION_KEY` has no recovery path
It encrypts SSH private keys, vault secrets, OIDC client secrets and console
+17 -5
View File
@@ -9,7 +9,7 @@ sidebar_label: Ports and networking
| Port | Service | Who connects | Expose publicly |
| ------- | ----------- | -------------------------------- | --------------- |
| `3000` | web | Browsers, via your reverse proxy | Yes, behind TLS |
| `8080` | server API | The web app | No, firewall it |
| `8080` | server API | Your reverse proxy | Not directly — proxied |
| `9090` | server gRPC | Agents | **Yes** |
| `4822` | guacd | The server | No, firewall it |
| `27017` | MongoDB | The server | No |
@@ -20,8 +20,8 @@ sidebar_label: Ports and networking
```mermaid
flowchart LR
B["Browser"] -->|HTTPS| P["Reverse proxy"]
P --> W["web :3000"]
W --> S["server :8080"]
P -->|"everything else"| W["web :3000"]
P -->|"/api /auth /public /install* /update*"| S["server :8080"]
A["Agent on a managed server"] -->|"gRPC/TLS :9090, outbound"| S
S --> G["guacd :4822"]
G -->|"relayed over the :9090 stream"| A
@@ -77,8 +77,20 @@ On a private network you can skip TLS instead, by setting `tls: false` in each
## Reverse proxy notes
- Point the proxy at `web:3000`. The web app reaches the API internally, so
`8080` does not need publishing.
- **The proxy routes two backends on one hostname**, and both are required:
| Path | Backend |
| ------------------------------------------------------------- | ------------- |
| `/api`, `/auth`, `/public`, `/install`, `/install.ps1`, `/update`, `/update.ps1` | `server:8080` |
| everything else | `web:3000` |
The web app forwards nothing to the API. Sending the whole hostname to
`web:3000` loads the interface and every request it makes answers `404`
including the login form.
- Both backends must be the **same** hostname and certificate. The browser
calls `/api` relative to the page it is on, and the session cookie is
host-only.
- The console uses a **WebSocket** at `/api/console/tunnel`. A proxy that does
not forward upgrade headers breaks the console and nothing else.
- Workflow log streaming is a long-lived response. A short proxy read timeout
+12
View File
@@ -20,6 +20,13 @@ or is not 64 hex characters.
## Nobody can sign in
**Every request 404s and the interface loads fine.** Your reverse proxy sends
the whole hostname to `web:3000`. `/api`, `/auth`, `/public`, `/install*` and
`/update*` belong to `server:8080` and the web app forwards nothing — see
[Ports and networking](./ports-and-networking.md#reverse-proxy-notes). The
tell is `curl -si https://<your-host>/auth/bootstrap-status` returning HTML
with `x-powered-by: Next.js` instead of JSON.
**`/setup` appears when users already exist.** The server is pointed at a
different database than you think. Check the database name in `MONGO_URI`,
which is taken from the end of the URI.
@@ -169,6 +176,11 @@ is `<your-instance>.vantage.<yourdomain>/status/<page-id>`, the same
per-instance subdomain everything else in Vantage uses. A wrong or missing
subdomain resolves to no instance at all, which is also a 404.
Third possibility: `/public` is not routed to the server. Check with
`curl -si https://<your-instance>.vantage.<yourdomain>/public/status/<page-id>`
— JSON is correct, HTML carrying `x-powered-by: Next.js` means the proxy sent
that prefix to the web app.
**Loads, but shows an explanation instead of components.** This is not a
fault — it is the page working as designed. It means either the licence has
lapsed (a self-hosted instance past its grace period, or a cloud instance
+23 -5
View File
@@ -22,9 +22,19 @@ of lowercase letters, digits and `-`, starting and ending with a letter or
digit. It becomes part of the public URL:
```
https://<your-instance>.vantage.<yourdomain>/status/<page-id>
https://<your-vantage-address>/status/<page-id>
```
On **Vantage Cloud** that address is your instance's own subdomain, so the page
is at `https://<your-instance>.vantage.hostxtra.co.uk/status/<page-id>`.
On a **self-hosted** install it is whatever address you reach Vantage on —
`https://vantage.acme.com/status/<page-id>`, or an IP and port on a LAN
install. A self-hosted install serves exactly one Vantage instance, so no
subdomain is needed to say which one you mean. The **Copy** control next to the
page address in the editor gives you the exact URL for your install, which is
the one to hand out.
**The page id cannot be changed after creation.** Once you have shared the
link, changing the id would break it, so pick something you would still be
happy with in a year — `platform`, `api`, a customer's own name for a
@@ -38,6 +48,11 @@ so you can build out the components and copy before announcing it. Toggle
**Published** when it is ready. Un-publishing later takes it back to *not
found* rather than deleting anything.
**Delete page**, in the editor header, is the only way to correct a page id you
regret — the id is fixed once created. It takes the page, its sections and its
authored incidents with it; monitors and their history are untouched. If you
only want the page off the internet, un-publish it instead.
## Sections and components
A page is organised into **sections** — arbitrary groupings such as "API" or
@@ -55,12 +70,15 @@ more and claiming otherwise would be a false claim of health.
## What a visitor sees
- Component name, current state (up / down / under maintenance / unknown) and
a 90-day uptime percentage.
- Component name, current state (up / down / under maintenance / pending /
unknown) and a 90-day uptime percentage. **Pending** is a monitor that has
been added but has not produced a result yet; **unknown** is one nothing is
checking any more.
- A 90-day history bar per component.
- Any active incidents, upcoming maintenance, and a rolling history of both.
- An optional banner across the top of the page (info / warning / critical),
for anything you want said regardless of component state.
- An optional banner across the top of the page, for anything you want said
regardless of component state. It is one notice with one appearance — there
are no severity levels to choose between.
A visitor never sees a target URL, host or port, the check's expected status
or keyword, latency, a certificate expiry date, failure text, or which
+1 -1
View File
@@ -49,7 +49,7 @@ const sidebars: SidebarsConfig = {
{
type: "category",
label: "Operations",
items: ["operations/upgrading", "operations/backups", "operations/agent-updates"],
items: ["operations/upgrading", "operations/backups", "operations/backup-and-restore", "operations/agent-updates"],
},
],
};