chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md
Deploy / deploy (push) Successful in 2m47s
Deploy / deploy (push) Successful in 2m47s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user