chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md
Deploy / deploy (push) Successful in 2m47s

This commit is contained in:
2026-09-10 09:18:57 +00:00
parent e8455c99fe
commit 7762629634
22 changed files with 126 additions and 122 deletions
+19 -19
View File
@@ -5,7 +5,7 @@ sidebar_label: API tokens
---
A session cookie is fine for a browser. A script, a CI job or a cron task
needs something it can hold onto instead an API token.
needs something it can hold onto instead - an API token.
## Creating one
@@ -15,7 +15,7 @@ admin additionally see every key in the instance.
**Create key** opens a dialog with four decisions, in the order they matter:
1. **Name and role.** The name says what will use the key the CI pipeline,
1. **Name and role.** The name says what will use the key - the CI pipeline,
the script, the cluster. The role list offers your own role and everything
below it, never above.
2. **Scopes.** A grid of resources against read and write. Ticking write also
@@ -26,7 +26,7 @@ admin additionally see every key in the instance.
4. **Expiry.** Each option names the date it resolves to, so "90 days" and
"7 December 2026" are the same choice read two ways.
Beneath them sits a preview line that reads the key back as a sentence
Beneath them sits a preview line that reads the key back as a sentence -
*"gitea-ci-deploy acts as admin, may read and write servers and workflows,
read secrets, and stops working on 7 December 2026."* Read it before you
submit; an over-granted key is far easier to spot in a sentence than in a grid
@@ -40,7 +40,7 @@ vt_8f2c1a9e4b6d0735a1c8e29f4b0d6e17...
That is the only time you will see it. Vantage stores a hash of the token,
never the value itself, so if you lose it there is no support ticket that gets
it back create a new token and revoke the old one.
it back - create a new token and revoke the old one.
The panel showing it also carries a ready-made `curl` line and a summary of
what was granted, so the key can go straight into a secret store without a
@@ -50,7 +50,7 @@ second trip to the list.
A token can reach only what its scopes name. Each resource has
a `:read` and a `:write` scope, and holding `:write` on a resource also
satisfies a `:read` requirement for it you do not need to tick both.
satisfies a `:read` requirement for it - you do not need to tick both.
| Resource | Covers |
| ----------- | --------------------------------------------------- |
@@ -64,7 +64,7 @@ satisfies a `:read` requirement for it — you do not need to tick both.
| `settings` | Instance settings, members, single sign-on, licence, and token management itself |
A token created with only `servers:read` can list and inspect servers but
cannot run a workflow against them, touch a key, or read a secret each of
cannot run a workflow against them, touch a key, or read a secret - each of
those needs its own scope.
## Tag restrictions
@@ -72,12 +72,12 @@ those needs its own scope.
A key can be pinned to part of the fleet as well as part of the API. **Restrict
to servers tagged** in the create dialog offers the tag keys and values already
in use across your servers, and the key then reaches only servers carrying
**every** pair listed the restriction is an AND, not an OR. Leaving it empty
**every** pair listed - the restriction is an AND, not an OR. Leaving it empty
is the opposite: no restriction at all, the whole fleet.
The restriction is fixed at creation, like the role and the scopes. Changing
what a credential already deployed in CI can reach, with no record of what it
could reach before, is worse than requiring a rotation so to widen or narrow
could reach before, is worse than requiring a rotation - so to widen or narrow
one, create a replacement and revoke the old key.
Restricted keys show their tags as chips beside their scopes in the list.
@@ -86,7 +86,7 @@ Unrestricted keys show nothing there, which is the common case.
The restriction applies to every API token, not only ones handed to an
[MCP agent](../vantage/mcp.md). A CI token, a monitoring script's token and
an agent's token are all held to the same tag scope check wherever the
service layer resolves servers the mechanism does not know or care what
service layer resolves servers - the mechanism does not know or care what
kind of caller is holding the token.
Nor can a token created under a restricted token reach further than its
@@ -98,27 +98,27 @@ token that also sees production.
Each key is one record rather than a row of bare strings:
- **Key** the name, the `vt_` prefix hint, the holder (when viewing all
- **Key** - the name, the `vt_` prefix hint, the holder (when viewing all
keys) and the role.
- **Scopes** one chip per resource, its access half tinted: `rw` in accent,
- **Scopes** - one chip per resource, its access half tinted: `rw` in accent,
`r` in grey. A key with nothing granted says so in words rather than showing
a dash.
- **Lifetime** a bar showing how much of the key's issued life is left,
- **Lifetime** - a bar showing how much of the key's issued life is left,
with the date beside it. Four states, and the label always says which:
| Bar | Means |
| --- | ----- |
| Green | More than seven days left |
| Amber | Seven days or fewer rotate it |
| Amber | Seven days or fewer - rotate it |
| Red | Already expired; the key no longer authenticates |
| Grey, full width | No expiry at all |
A key issued before the instance's maximum lifetime was tightened also
carries *"outside the current policy rotate when convenient"*. That is a
carries *"outside the current policy - rotate when convenient"*. That is a
prompt, not a failure: the cap is never applied retroactively and the key
keeps working.
- **Last call** when the key last authenticated, or *Never used*.
- **Last call** - when the key last authenticated, or *Never used*.
Above the list, four counts summarise the same thing at fleet scale: keys
listed, keys expiring within seven days, keys that never expire, and keys
@@ -128,7 +128,7 @@ change with the **My keys** / **All keys** toggle.
## A token never outranks its owner
A token's role can be at most the role of the person who created it, and its
effective role is **recomputed on every request** as the lower of the two
effective role is **recomputed on every request** as the lower of the two -
not fixed at creation. Demote the person from owner to member and every token
they hold drops to member from that request onward. Remove the person and
every token they hold stops working immediately: a token has no existence
@@ -154,7 +154,7 @@ curl -H "Authorization: Bearer vt_…" https://acme.vantage.example.com/api/serv
```
Everything else about the [REST API](./rest-api.md) applies the same way it
does to a session JSON errors, audit logging, licence gating on writes
does to a session - JSON errors, audit logging, licence gating on writes -
except that authority comes from the token's role and scopes rather than a
signed-in person's role.
@@ -178,12 +178,12 @@ already been deleted.
This page covers the token model. Every route, request and response shape is
in the generated OpenAPI reference, served by **your own instance** at
`/api/docs` not this documentation site, since the routes and their shapes
`/api/docs` - not this documentation site, since the routes and their shapes
are specific to your install. The raw document is at `/api/openapi.json`.
:::danger Not the External Secrets token
The bearer token read by `GET /api/secrets/:group/values` for the Kubernetes
External Secrets Operator is a **separate credential** a single instance-wide
External Secrets Operator is a **separate credential** - a single instance-wide
value, rotated from Settings, that reaches only that one endpoint. It is not an
API token and an API token cannot be used in its place: the two are checked by
different code, and neither substitutes for the other. See
+1 -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. **On a LAN-only install, narrow this to your proxy's address.** The RFC1918 default trusts every private range, so a client on 192.168.0.0/16 reaching the server directly is itself a "trusted proxy" and can put whatever it likes in `X-Forwarded-For` and, on the public status route, in `X-Forwarded-Host`. Behind a proxy on a public address, or with no proxy at all, that is not reachable; on a flat LAN it is |
| `TRUSTED_PROXIES` | no | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | Comma-separated CIDRs or addresses of proxies allowed to set `X-Forwarded-For`. The shipped Docker Compose and Helm chart default to the private RFC1918 ranges, which covers Nginx Proxy Manager on the Docker bridge network and Traefik on a Kubernetes pod CIDR. An operator whose proxy sits on a public address must set this themselves, or every visitor behind it shares one address for rate-limiting purposes. Unset entirely (outside those shipped defaults) trusts none, so the client address is the direct peer. **On a LAN-only install, narrow this to your proxy's address.** The RFC1918 default trusts every private range, so a client on 192.168.0.0/16 reaching the server directly is itself a "trusted proxy" and can put whatever it likes in `X-Forwarded-For` - and, on the public status route, in `X-Forwarded-Host`. Behind a proxy on a public address, or with no proxy at all, that is not reachable; on a flat LAN it is |
:::danger `KEY_ENCRYPTION_KEY` has no recovery path
It encrypts SSH private keys, vault secrets, OIDC client secrets and console
+2 -2
View File
@@ -9,7 +9,7 @@ sidebar_label: Ports and networking
| Port | Service | Who connects | Expose publicly |
| ------- | ----------- | -------------------------------- | --------------- |
| `3000` | web | Browsers, via your reverse proxy | Yes, behind TLS |
| `8080` | server API | Your reverse proxy | Not directly proxied |
| `8080` | server API | Your reverse proxy | Not directly - proxied |
| `9090` | server gRPC | Agents | **Yes** |
| `4822` | guacd | The server | No, firewall it |
| `27017` | MongoDB | The server | No |
@@ -85,7 +85,7 @@ On a private network you can skip TLS instead, by setting `tls: false` in each
| everything else | `web:3000` |
The web app forwards nothing to the API. Sending the whole hostname to
`web:3000` loads the interface and every request it makes answers `404`
`web:3000` loads the interface and every request it makes answers `404` -
including the login form.
- Both backends must be the **same** hostname and certificate. The browser
+6 -6
View File
@@ -22,7 +22,7 @@ or is not 64 hex characters.
**Every request 404s and the interface loads fine.** Your reverse proxy sends
the whole hostname to `web:3000`. `/api`, `/auth`, `/public`, `/install*` and
`/update*` belong to `server:8080` and the web app forwards nothing see
`/update*` belong to `server:8080` and the web app forwards nothing - see
[Ports and networking](./ports-and-networking.md#reverse-proxy-notes). The
tell is `curl -si https://<your-host>/auth/bootstrap-status` returning HTML
with `x-powered-by: Next.js` instead of JSON.
@@ -124,7 +124,7 @@ User-Agent: Vantage-Monitor/1.0 (+https://vantage.hostxtra.co.uk)
That string is the hook to allow the check through. In whichever product is
doing the blocking, add a rule that skips bot protection, managed rules and rate
limiting for requests carrying it Cloudflare, AWS WAF, Azure Front Door,
limiting for requests carrying it - Cloudflare, AWS WAF, Azure Front Door,
Akamai, Fastly, Imperva, Sucuri, ModSecurity, nginx and HAProxy all match on a
request header. The shape of the rule is the same everywhere:
@@ -137,7 +137,7 @@ Three details are worth getting right:
exact match breaks silently on an upgrade, and the symptom is a monitor that
goes down on deploy day.
- **Keep the rule narrow.** Scope it to the specific host and path being
monitored. A User-Agent is not a secret anyone can send it so a rule that
monitored. A User-Agent is not a secret - anyone can send it - so a rule that
skips protection site-wide on that string alone is a bypass you have
published.
- **Allow the source address too, where you can.** Combining the User-Agent with
@@ -170,7 +170,7 @@ needs `host`, `port`, `from` and `to`, and Telegram needs both `token` and
## A status page 404s or shows no data
**404, and it should be published.** Check the **Published** toggle on the
page's editor an unpublished page answers *not found* for everyone,
page's editor - an unpublished page answers *not found* for everyone,
including you, with no session exemption. Also check the host: the public URL
is `<your-instance>.vantage.<yourdomain>/status/<page-id>`, the same
per-instance subdomain everything else in Vantage uses. A wrong or missing
@@ -178,11 +178,11 @@ subdomain resolves to no instance at all, which is also a 404.
Third possibility: `/public` is not routed to the server. Check with
`curl -si https://<your-instance>.vantage.<yourdomain>/public/status/<page-id>`
JSON is correct, HTML carrying `x-powered-by: Next.js` means the proxy sent
- JSON is correct, HTML carrying `x-powered-by: Next.js` means the proxy sent
that prefix to the web app.
**Loads, but shows an explanation instead of components.** This is not a
fault it is the page working as designed. It means either the licence has
fault - it is the page working as designed. It means either the licence has
lapsed (a self-hosted instance past its grace period, or a cloud instance
between billing events) or the current tier does not include the **Status
pages** feature. Fix the licence or the plan and the same link starts serving