diff --git a/CLAUDE.md b/CLAUDE.md index 3097d5c..6e73679 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,21 @@ Vault KV v2 path structure: - `/shared//` — shared across all instances - `/instance///` — instance-specific - `/.config` — folder metadata: `{"allowed_groups":["group1"], "description":"..."}` +- `/users/` — per-user credential variables + +#### Multiple Vault backends (DR) + +Optional `[vault_shared]` / `[vault_local]` blocks (same keys as `[vault]`) give +the `shared` / `instance` scopes their own Vault so one being down can't take the +other with it. A bare `[vault]` is unchanged (shared+local both alias it). Secret +IDs: `VAULT_SECRET_ID`, `VAULT_SHARED_SECRET_ID`, `VAULT_LOCAL_SECRET_ID`. Each +backend connects/retries/renews independently; a down backend greys that scope in +the UI. Per-credential scope: a credential variable can be stored shared or local +(location = truth), toggled per-row in My Credentials (hidden with a single +Vault); `user_credentials_default_scope` (default `local`) seeds new ones. Split +an existing single-Vault deployment with `rustguac vault-migrate` (copy subtree + +.config, then add the block + restart — routing is single-source, no read +fallback). Implemented on branch `feature/multi-vault-dr` (see project memory). ### OIDC diff --git a/docs/configuration.md b/docs/configuration.md index b8e1e73..8359b00 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -134,6 +134,49 @@ Enables the Vault-backed connections. Requires `VAULT_SECRET_ID` environment var | `client_cert` | — | Path to client certificate (PEM) for mTLS | | `client_key` | — | Path to client private key (PEM) for mTLS (required if `client_cert` is set) | +### Multiple Vault backends (disaster recovery) + +By default the single `[vault]` serves both the shared and instance (local) +address-book scopes. For DR across a fleet you can give each scope its own Vault +so that one being unreachable cannot take the other down with it. Add either or +both of the optional blocks below; each takes the same keys as `[vault]`. + +| Block | Serves | Secret ID env var | +|-------|--------|-------------------| +| `[vault]` | Default/fallback for any scope without a dedicated backend; also the home of the LUKS key | `VAULT_SECRET_ID` | +| `[vault_shared]` | The `shared` scope | `VAULT_SHARED_SECRET_ID` | +| `[vault_local]` | The `instance` (local) scope | `VAULT_LOCAL_SECRET_ID` | + +A bare `[vault]` with no overrides behaves exactly as a single-Vault deployment, +so nothing changes for existing installs. Each backend connects, retries, and +renews its token independently. If a dedicated backend is unreachable, that +scope is shown as temporarily unavailable in the Connections tree while the +other scopes keep working. + +When `[vault_local]` is used, set its `instance_name` to the value the data was +originally stored under, so the `instance//` paths line up. Splitting an +existing single-Vault deployment is a one-time copy with the `vault-migrate` +subcommand (see the [migration guide](migration.md)). + +```toml +# Primary/local Vault (always reachable on this instance) +[vault] +addr = "https://127.0.0.1:8200" +role_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +instance_name = "dc1" + +# Optional central Vault shared across the fleet +[vault_shared] +addr = "https://vault-central.example.com:8200" +role_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" +``` + +Related top-level setting: + +| Key | Default | Description | +|-----|---------|-------------| +| `user_credentials_default_scope` | `local` | Where a new per-user credential variable is stored when more than one backend is configured: `local` (stays on this instance, survives a central outage) or `shared` (propagates fleet-wide). Ignored with a single Vault. | + ## `[drive]` section Enables file transfer for RDP (drive redirection) and SSH (SFTP). @@ -327,7 +370,9 @@ home_base = "/vdi-homes" | Variable | Description | |----------|-------------| | `OIDC_CLIENT_SECRET` | Override OIDC client secret from config file | -| `VAULT_SECRET_ID` | Vault AppRole secret ID | +| `VAULT_SECRET_ID` | Vault AppRole secret ID for `[vault]` | +| `VAULT_SHARED_SECRET_ID` | Vault AppRole secret ID for `[vault_shared]` (only if configured) | +| `VAULT_LOCAL_SECRET_ID` | Vault AppRole secret ID for `[vault_local]` (only if configured) | | `RUST_LOG` | Log level (e.g., `info`, `debug`, `rustguac=debug`) | ### Setting environment variables for systemd diff --git a/docs/credential-variables.md b/docs/credential-variables.md index 44bf0da..8aeac03 100644 --- a/docs/credential-variables.md +++ b/docs/credential-variables.md @@ -1,6 +1,6 @@ # Credential Variables -Credential variables let connections entries reference shared credentials by name instead of storing passwords directly. Users maintain their own credential values in Vault via the **My Credentials** dialog (gear menu). When a session launches, rustguac substitutes the variables from the user's saved values. +Credential variables let connections entries reference shared credentials by name instead of storing passwords directly. Users maintain their own credential values in Vault via the **My Credentials** dialog (the **Credentials** link in the top navigation, or the gear menu). When a session launches, rustguac substitutes the variables from the user's saved values. This gives a similar experience to LDAP credential passthrough in Apache Guacamole — users log in once and sessions just work — without rustguac needing to bind to LDAP. Credentials stay in Vault, never on disk or in the browser. @@ -53,19 +53,48 @@ VDI entries that auto-derive credentials (the default for images that honour `VD ## My Credentials dialog -Access via the gear icon in the top-right corner of the connections page. The dialog: +Open it from the **Credentials** link in the top navigation bar, or from the +gear menu. The dialog: - Shows all credential variables used across entries the user has access to -- Groups variables by domain prefix +- Groups variables by domain prefix, with each group collapsible +- Provides a filter box (shown once the list is long) to find a variable by name or domain - Indicates how many entries use each variable - Masks password and key fields (saved values are not shown, but a placeholder confirms they exist) -- Partial saves work — fill in what you have now, come back later for the rest +- Scrolls its body while the title and the Save/Close buttons stay pinned, so it stays usable with many variables +- Supports partial saves: fill in what you have now, come back later for the rest + +### Finding what needs setting up + +The Connections page surfaces missing credentials before a connection fails: + +- A **"credentials needed"** pill marks entries that reference a variable you have not set yet. +- A dismissible banner reports how many credentials are still to set up. The dismissal is remembered across refreshes and only reappears if a genuinely new unset credential shows up. ## Graceful degradation -- **All variables set** — session launches immediately, no prompting -- **Some missing** — credential prompt appears with known values pre-filled; user only needs to fill gaps -- **None set** — full credential prompt (same as entries without variables) +- **All variables set:** the session launches immediately, no prompting. +- **Some missing:** the connect opens My Credentials focused on just the missing variables, and the connection resumes automatically once you save them. +- **None set:** full credential prompt (same as entries without variables). + +## Shared and local credentials (multiple Vaults) + +When more than one Vault backend is configured (see +[Multiple Vault backends](configuration.md)), each credential can be stored in +the **shared** Vault (propagates to every site) or kept **local** to this +instance. My Credentials shows a **"Shared across sites"** checkbox per +credential; the default for a new credential comes from +`user_credentials_default_scope`. Reads merge both backends (a local value wins +over a shared one of the same name), and toggling a credential moves it between +the two. + +With a single Vault there is only one store, so the checkbox is hidden and every +credential is simply stored there. + +Trade-off to be aware of: a credential kept in the shared Vault will not resolve +while that Vault is unreachable, so a connection that references it fails even if +the target and the local Vault are up. Keep credentials a site must never lose +(for example break-glass logins) **local**. ## Vault storage @@ -75,7 +104,9 @@ User credentials are stored in Vault KV v2 at: /users/ ``` -Each user gets a single Vault secret containing all their credential key-value pairs. Variable names are the keys, plaintext values are the values. The Vault policy must allow read/write to this path for authenticated users. +Each user gets a single Vault secret (per backend) containing their credential key-value pairs. Variable names are the keys, plaintext values are the values. The Vault policy must allow read/write to this path for authenticated users. + +With multiple Vault backends configured, a user's credentials are split by scope: local credentials live at this path in the local backend, shared credentials at the same path in the shared backend. The `users/*` policy is needed on whichever backends hold credentials. ### Required Vault policy diff --git a/docs/migration.md b/docs/migration.md index 7073018..7c342e2 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -125,3 +125,56 @@ Once imported, connections appear in the connections UI. You can: - The import is additive: existing entries in the target folder are not deleted or overwritten. If you re-run the import, entries with the same name will be updated. - Guacamole user/group permissions are not imported. Use rustguac's OIDC group mappings and folder `allowed_groups` instead. - Credentials (passwords, private keys) are imported into Vault where they are stored encrypted at rest and never touch disk. + +# Splitting to multiple Vaults (disaster recovery) + +If you already run a single Vault serving both the `shared` and `instance` +scopes and want to move a scope onto a dedicated Vault (see +[Multiple Vault backends](configuration.md)), the `vault-migrate` subcommand +copies a scope's whole subtree between two configured backends. Because the +scope-to-path layout is identical in every backend, this is a same-identity +copy, not a rewrite: it moves the entries **and** each folder's access config +(`.config`), so `allowed_groups` and inheritance travel with them. + +## Step 1: Preview + +Configure the new backend block (e.g. `[vault_shared]`) and its +`VAULT_SHARED_SECRET_ID`, then dry-run the copy: + +```bash +VAULT_SECRET_ID=... VAULT_SHARED_SECRET_ID=... \ +rustguac --config /opt/rustguac/config.toml \ + vault-migrate --scope shared --from vault --to vault_shared --dry-run +``` + +## Step 2: Copy + +```bash +VAULT_SECRET_ID=... VAULT_SHARED_SECRET_ID=... \ +rustguac --config /opt/rustguac/config.toml \ + vault-migrate --scope shared --from vault --to vault_shared +``` + +### Options + +| Flag | Default | Description | +|------|---------|-------------| +| `--scope` | (required) | `shared` or `instance` | +| `--from` / `--to` | (required) | Backend names: `vault`, `vault_shared`, or `vault_local` | +| `--dry-run` | off | Preview without writing to the destination | +| `--overwrite` | off | Overwrite entries that already exist at the destination (default: skip existing) | +| `--users` | off | Also copy every per-user credential secret (`users/*`). This makes those credentials shared; normally you toggle per-credential in My Credentials instead. | + +## Step 3: Cut over + +Routing is deterministic and single-source: once `[vault_shared]` is configured, +the `shared` scope reads only from it, with no fall-back to `[vault]`. So the +order matters: + +1. Copy the subtree first (Step 2). +2. Then add the `[vault_shared]` block and restart rustguac. + +Doing it the other way round makes shared connections briefly disappear (the +data is safe in the old Vault, just not being read). Entries and folders are +single-source; only per-user credentials merge across backends, so those never +have a gap.