diff --git a/claude.md b/claude.md index 36a5f20..ad0ed92 100644 --- a/claude.md +++ b/claude.md @@ -138,7 +138,22 @@ Key/value pairs grouped by name, encrypted at rest with AES-256-GCM. Consumed tw ### Browser console -`POST /api/console/connect` mints a one-time session token; `GET /api/console/tunnel` upgrades to a WebSocket and proxies to **guacd** (Apache Guacamole daemon) using `github.com/wwt/guac`. SSH connections authenticate with a stored private key; RDP/VNC credentials are encrypted, single-use, and consumed when the tunnel opens. +`POST /api/console/connect` mints a one-time session token; `GET /api/console/tunnel` +upgrades to a WebSocket and proxies to **guacd** using `github.com/wwt/guac`. + +guacd never dials the managed server. The server binds a single-use ephemeral +listener, pushes `OpenProxyCmd` down the agent's command stream, and the agent +opens a `ProxyStream` and relays the connection from its own **`127.0.0.1`** — +the host is hardcoded agent-side, so the control plane can name only a port. +This is what makes the console work on Vantage Cloud, where the customer's +server is behind NAT on a private address. It also means the console now +**requires a live agent** on every deployment: `consoleConnect` answers 409 +`agent_offline` rather than hanging. + +SSH connections authenticate with a stored private key; RDP/VNC credentials are +encrypted, single-use, and consumed when the tunnel opens. None of them reach +the agent — the session is negotiated end-to-end between guacd and the target +daemon, so the agent relays bytes it cannot read. ### Inventory and OS updates @@ -517,6 +532,8 @@ Windows: MSI built by CI (WiX), or `installer/setup.ps1` registering the agent a | `REDIS_ADDR` | no | default `localhost:6379` | | `KEY_ENCRYPTION_KEY` | yes in practice | 64-char hex (32 bytes) for AES-256-GCM. Required for private keys, secrets, OIDC secrets, RDP credentials. | | `GUACD_ADDR` | no | default `guacd:4822` | +| `PROXY_ADVERTISE_HOST` | no | default `server`; the hostname guacd resolves the control plane by, handed to guacd as the relay's address. Wrong here and every console session fails at connect | +| `PROXY_LISTEN_HOST` | no | default `0.0.0.0`; the interface the ephemeral relay listener binds | | `APP_ROOT_LABEL` | no | default `vantage`; wrong value disables the host/session org guard | | `VANTAGE_WORKFLOW_LOG_DIR` | no | where run logs are written | | `FREE_INSTANCE_REAP_AFTER` | no | duration past a Free licence's expiry before the instance and all its data are deleted. **Empty disables the reaper, and empty is the default.** Set to `336h` in `docker-compose.site.yml` only — a self-hosted deployment must never reap. Must match admin's value, which only names the date in warning emails | diff --git a/docsite/docs/reference/environment-variables.md b/docsite/docs/reference/environment-variables.md index fe95e62..76e4187 100644 --- a/docsite/docs/reference/environment-variables.md +++ b/docsite/docs/reference/environment-variables.md @@ -17,6 +17,8 @@ it is absent. | `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 | +| `PROXY_ADVERTISE_HOST` | no | `server` | The hostname **guacd** uses to reach the control plane's console relay. Wrong here and every console session fails at connect with guacd unable to resolve the relay | +| `PROXY_LISTEN_HOST` | no | `0.0.0.0` | Interface the ephemeral relay listeners bind. Narrow it only if guacd shares a known interface | | `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 | diff --git a/docsite/docs/reference/ports-and-networking.md b/docsite/docs/reference/ports-and-networking.md index 983db52..e747fe2 100644 --- a/docsite/docs/reference/ports-and-networking.md +++ b/docsite/docs/reference/ports-and-networking.md @@ -24,7 +24,8 @@ flowchart LR 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"] + G -->|"relayed over the :9090 stream"| A + A -->|"SSH / RDP / VNC, loopback"| T["Target machine (same host as agent)"] ``` Two things are worth reading off that diagram. @@ -33,9 +34,13 @@ Two things are worth reading off that diagram. 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. +**The console rides the agent's connection too.** guacd never dials the target +directly; the server pushes a command down the agent's existing outbound gRPC +stream on `9090`, and the agent relays the protocol traffic from its own +loopback. No route from the control plane to the target's address is needed, +and no new inbound port opens on the target — the same connection that carries +key sync carries console traffic. This is what makes the console work for a +machine behind NAT on a private subnet, as long as its agent is online. ## What to open @@ -49,7 +54,9 @@ private subnet cannot be consoled, even though every other feature works. - `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. + +No route to the machines you intend to console is needed — that traffic rides +the agent's existing outbound `9090` connection instead. ### Outbound from a managed machine diff --git a/docsite/docs/reference/troubleshooting.md b/docsite/docs/reference/troubleshooting.md index acee941..d37119d 100644 --- a/docsite/docs/reference/troubleshooting.md +++ b/docsite/docs/reference/troubleshooting.md @@ -86,7 +86,7 @@ instantaneous. | 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 | +| Hangs at connecting | The agent is offline, or nothing is listening on the protocol port on the target's own loopback address. Check the audit log for `console.proxy_failed` — its reason names which | | Fails only in production | The reverse proxy is not forwarding WebSocket upgrade headers | ## Monitors report down when the service is up diff --git a/docsite/docs/vantage/browser-console.md b/docsite/docs/vantage/browser-console.md index 03b4ce4..f35afa1 100644 --- a/docsite/docs/vantage/browser-console.md +++ b/docsite/docs/vantage/browser-console.md @@ -15,17 +15,20 @@ to a **guacd** daemon and manages credentials around it. - `guacd` running and reachable from the server. The bundled Compose stack includes it; `GUACD_ADDR` defaults to `guacd:4822`. - `KEY_ENCRYPTION_KEY` set, since every credential involved is stored encrypted. -- Network reachability **from the control plane to the target** on the protocol - port. This is the one part of Vantage that is not agent-mediated: guacd - connects directly, so a machine reachable only by its agent cannot be - consoled. +- The target's **agent must be online**. Console traffic is relayed over the + agent's existing outbound connection, so the control plane never needs a route + to the server's address — but it does need the agent. +- No inbound port on the target, beyond what the protocol already listens on + locally. A service bound only to `127.0.0.1` works, because the agent dials + loopback on the target itself. ## Opening a session From a server's page, choose **Console**. Then: 1. The UI calls `POST /api/console/connect`, which mints a **one-time** session - token. + token. If the target's agent is not connected, this fails immediately with + `409 agent_offline` rather than hanging. 2. The browser opens a WebSocket to `GET /api/console/tunnel` with that token. 3. The server marks the token consumed atomically, so a second use cannot race and proxies the connection to guacd. @@ -67,4 +70,4 @@ keystroke log. If you need that, it has to come from the target machine. | Connects then closes immediately | guacd unreachable check `GUACD_ADDR` and that the container is up | | SSH refuses the key | The stored key has no private half, or is not in the target's `authorized_keys` | | RDP fails on a fresh credential | Credentials are consumed on open; a retry needs them entered again | -| Hangs at connecting | The control plane cannot reach the target on the protocol port | +| Hangs at connecting | The agent is offline, or nothing is listening on the protocol port on the target's own loopback address. Check the audit log for `console.proxy_failed` — its reason names which |