From 68160dc681b5ec3be9e6c3794fc7d9d47105173b Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 25 Aug 2026 09:05:06 +0000 Subject: [PATCH] docs: correct the status page URL for self-hosted, trim to what ships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The status page URL was given only as `.vantage.`, 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. --- CLAUDE.md | 27 ++++++++++++++++-- .../docs/reference/environment-variables.md | 2 +- docsite/docs/vantage/status-pages.md | 28 +++++++++++++++---- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f5f1098..3ff3aa0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -503,6 +503,25 @@ with `available:false` and a `reason`, never a 403 or a blank page — the reader is a member of the public who can do nothing about either condition and deserves an explanation, not a browser error. +**The instance is resolved from `X-Forwarded-Host`, not `Host`.** The public +page is server-rendered by `web/`, and the SSR fetch cannot set `Host` at all: +it is a forbidden header name and undici drops it silently, so the Go server +saw `server:8080` and every status page 404'd on every deployment. `web/` +forwards the visitor's host in `X-Forwarded-Host` (and their address in +`X-Forwarded-For`, or the whole deployment shares one rate-limit bucket), and +`publicStatusInstance` honours that header **only when `c.RemoteIP()` is in +`TRUSTED_PROXIES`** — it selects a tenant, so an untrusted peer must not be +able to name one. It uses `RemoteIP()` and not `ClientIP()` deliberately: the +latter is reconstructed from the very headers being judged. + +**A host naming no slug falls back to the sole instance on a non-cloud +deployment.** `hostSlug` requires `.vantage.`; a self-hosted install +at `vantage.acme.com` or an IP has no slug and would otherwise 404 forever. It +has exactly one instance, resolved with the same count-then-read bootstrap +uses, cached alongside the slug lookups. More than one instance is a 404, not a +guess. A host that *does* name a slug which does not exist stays a 404 — +falling back there would serve one tenant's page on another's address. + Assembled snapshots are cached in Redis for **30 seconds**, keyed per instance and page, and every authoring write (`UpdateStatusPage`, `DeleteStatusPage`, and every incident mutation) invalidates its page's entry @@ -537,9 +556,9 @@ only as sha256 — the same shape as `servers.agent_token_hash` and the ESO read token, and for the same reason: nothing downstream ever needs the plaintext back. It belongs to the user who created it, and its role can never exceed theirs; see the `api_tokens` note under MongoDB Collections for how that stays -true across a demotion rather than only at issuance. Scopes are eight +true across a demotion rather than only at issuance. Scopes are nine resources — `servers`, `keys`, `secrets`, `workflows`, `monitors`, `vulns`, -`workloads`, `settings` — each split into `:read` and `:write`, with `:write` +`workloads`, `status`, `settings` — each split into `:read` and `:write`, with `:write` satisfying a `:read` requirement on the same resource so a caller does not have to hold both. Any signed-in member may mint and revoke their **own** tokens — there is no `RequireRole` on `POST /tokens` or `DELETE /tokens/:id` — because @@ -818,6 +837,10 @@ workloads GET /workloads · GET /servers/:id/workloads POST /servers/:id/workloads/refresh POST /servers/:id/workloads/:wid/action (owner|admin) GET /servers/:id/workloads/:wid/logs (owner|admin) +status-pages GET,POST /status-pages · GET,PUT,DELETE /status-pages/:pageId (owner|admin) + GET,POST /status-pages/:pageId/incidents + PUT,DELETE /status-pages/:pageId/incidents/:incidentId + POST /status-pages/:pageId/incidents/:incidentId/updates audit GET /audit agent GET /agent/latest-version settings GET,PUT /settings · POST /settings/secrets-token (owner|admin) diff --git a/docsite/docs/reference/environment-variables.md b/docsite/docs/reference/environment-variables.md index ab09677..9270c51 100644 --- a/docsite/docs/reference/environment-variables.md +++ b/docsite/docs/reference/environment-variables.md @@ -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 diff --git a/docsite/docs/vantage/status-pages.md b/docsite/docs/vantage/status-pages.md index 90f000f..3e78553 100644 --- a/docsite/docs/vantage/status-pages.md +++ b/docsite/docs/vantage/status-pages.md @@ -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://.vantage./status/ +https:///status/ ``` +On **Vantage Cloud** that address is your instance's own subdomain, so the page +is at `https://.vantage.hostxtra.co.uk/status/`. + +On a **self-hosted** install it is whatever address you reach Vantage on — +`https://vantage.acme.com/status/`, 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