docs: document multi-Vault backends, per-credential scope, My Credentials UX

- configuration.md: [vault_shared]/[vault_local] backends, the DR rationale,
  user_credentials_default_scope, and the VAULT_SHARED/LOCAL_SECRET_ID env vars.
- credential-variables.md: Credentials nav link, scroll/filter/collapsible
  editor, resumable focused prompt, needed-pill + banner, and the shared/local
  per-credential scope with its outage trade-off.
- migration.md: vault-migrate runbook (dry-run, copy, cutover ordering).
- CLAUDE.md: multi-Vault backend summary under the Vault section.
This commit is contained in:
Dave Kempe
2026-07-29 20:07:51 +10:00
parent 842aeec8f1
commit ba207010b7
4 changed files with 153 additions and 9 deletions
+15
View File
@@ -59,6 +59,21 @@ Vault KV v2 path structure:
- `<base_path>/shared/<folder>/<entry>` — shared across all instances
- `<base_path>/instance/<name>/<folder>/<entry>` — instance-specific
- `<folder>/.config` — folder metadata: `{"allowed_groups":["group1"], "description":"..."}`
- `<base_path>/users/<sanitized_email>` — 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
+46 -1
View File
@@ -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/<name>/` 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
+39 -8
View File
@@ -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:
<base_path>/users/<sanitized_email>
```
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
+53
View File
@@ -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.