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:
@@ -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.
|
||||
Reference in New Issue
Block a user