Headline changes since v1.6.5:
- Fix zombie WebSocket: wire tunnel.onerror / onstatechange in
client.html so a mid-path WS drop surfaces the disconnected
overlay instead of leaving the tab frozen.
- Reconnect button now relaunches the original Connections entry
via /api/addressbook/.../connect rather than reloading a
Completed session URL. Ad-hoc and shareToken paths fall back
cleanly. Bonus: client.onerror clears the thumbnail upload
interval so the secondary leak (XHR 404s against a dead session)
stops the moment the overlay shows.
- v1.6.6 polish (already on main): OIDC discovery error wrapping
for trailing-slash mismatches, contrib/setup-xrdp-gfx.sh adds
xrdp to ssl-cert and normalises key.pem perms, aurora theme
applies when [theme] is absent (not just empty), new
docs/reverse-proxies.md covering nginx / Caddy / Apache /
Traefik with the %2F-decoding gotcha.
- Dependency bumps: rustls-webpki 0.103.13 (RUSTSEC-2026-0104,
CRL-parse panic + URI excluded-subtree fix), rustls 0.23.39,
russh 0.60.1, libc 0.2.186, plus matching /fuzz mirrors.
- Test cleanup: drop format!("{}", ...) and field-reassign-after-
Default patterns flagged by clippy 1.93.
Deferred to v1.6.7:
- RustCrypto batch (aes 0.9 + cbc 0.2 + hmac 0.13 + pbkdf2 0.13)
tracked in #117. They share digest 0.11 traits and have to land
together; individual dependabot PRs (#107/#109/#111/#113) closed
in favour of one coordinated commit.
- rand 0.10 (#108): API breaking, no security pressure, will get
picked up next time token generation paths are touched.
The previous commit (Fix zombie WebSocket) wired tunnel.onerror /
onstatechange into client.onerror so the disconnected overlay
appears when a mid-path WS drop is detected. That fixes the
"frozen tab" symptom but leaves the Reconnect button doing
window.location.reload(), which re-opens client.html?session=<id>
against a session that no longer exists. By the time the WS
proxy task in src/websocket.rs returns BrowserEnded, disconnect_viewer
has decremented active_connections to 0 and complete_session has
flipped status to Completed; the guacd stream was dropped when the
proxy task ended. So a reload just walks back into the same overlay.
Reconnect now relaunches against the original Connections entry
when one exists, which is the user's actual intent. The fetch at
the top of setupClient already pulls /api/sessions/:id; we now
stash address_book_entry, address_book_folder, and
entry_display_name from that response into a relaunchInfo object.
On Reconnect click:
- shareToken viewer: reload (unchanged; nothing better to offer).
- relaunchInfo present: POST to
/api/addressbook/folders/{scope}/{folder}/entries/{entry}/connect
with the current window dims and DPI, then navigate to
data.client_url + ?name=<display>. Fresh session id, fresh
guacd connection, RDP/xrdp resumes the desktop state on the
server side.
- relaunchInfo absent (ad-hoc) or any non-2xx (412 missing creds,
403 forbidden, 404 entry deleted) or network error: redirect to
/connections.html so the user can fill in what is missing or
pick somewhere else to go.
Splitting scope/folder/entry from the composite entry_key relies
on the constraint enforced in vault.rs validate_name: entry names
cannot contain slashes. Folder paths can, but only ever sit
between scope and entry, so first-slash gives scope and last-slash
gives entry.
Bundled fix: client.onerror now clearInterval(_thumbInterval) when
it runs. That closes the secondary leak called out in the prior
commit message (thumbnail XHR uploader continuing to 404 against
an already cleaned-up session) for users who walk away from the
overlay rather than clicking Close or Reconnect immediately.
Web sessions (Chromium + Xvnc) are entry-backed too and will
relaunch via the same path. The new session means a fresh
Chromium process with empty tab state, but that matches what the
user expects from Reconnect on a web session anyway.
When a mid-path middlebox silently drops the TCP between the browser
and HAProxy, HAProxy closes the backend which rustguac logs as
"Connection reset without closing handshake" (termination state CD--
in HAProxy's log). Firefox's WS socket then fires onclose on the
browser side, Tunnel.js's close_tunnel() runs, and the tunnel's
internal state transitions to CLOSED.
But upstream Guacamole's Client.js doesn't listen for tunnel.onerror
or tunnel.onstatechange — the Apache webapp's AngularJS wiring does
that externally. Our lean client.html inherited Tunnel.js + Client.js
verbatim from upstream but missed that glue, so tunnel errors fired
into the void and the Guacamole client stayed in CONNECTED forever.
Effect on users: a dead session that looks like a freeze. Mouse moves
locally, clicks don't register (they're being sent into a closed
WebSocket whose send() silently no-ops at the browser layer). The
"Session Ended" overlay never appears. Meanwhile the thumbnail
uploader keeps running on its own XHR stream, getting 404s from the
already-cleaned-up session.
Fix: wire tunnel.onerror to forward into the existing client.onerror
handler, and wire tunnel.onstatechange so CLOSED also triggers the
overlay (and UNSTABLE updates the status text). This reuses the
existing overlay wiring at client.html:895; no new UI, no new state
machine, no new heartbeat. The upstream-inherited 5s nop keepalive
and 15s receiveTimeout in Tunnel.js are already present and
correct — the missing piece was just propagating their output into
the client's state cascade.
Triggered a lot of detective work chasing false leads (H.264 decoder
closed-state hypothesis, h2 bridging bug, HAProxy timeout tuning,
client/server heartbeats). The actual cause was much simpler and
the diff is six lines of JavaScript.
### OIDC discovery error wrap
src/oidc.rs now reshapes the openidconnect crate's "unexpected issuer
URI `X` (expected `Y`)" validation error into something an operator
can act on without having to reason about which side is the config
and which is the provider. For the overwhelmingly common
trailing-slash case the message collapses to a single actionable
line; for the rarer case where the two URIs differ materially (wrong
tenant, copy-pasted authorise URL, Keycloak path change), it falls
back to a config/provider/fix three-liner. Raw Debug output is
preserved for any non-mismatch discovery failure. Works for any
OIDC provider, not just JumpCloud.
### xrdp TLS key permissions (contrib/setup-xrdp-gfx.sh)
After rebuilding xrdp from sid, the `adduser xrdp ssl-cert` step
from the Debian postinst doesn't always re-apply cleanly, leaving
the xrdp user unable to read /etc/xrdp/key.pem. xrdp then falls
back to "classic RDP security" and FreeRDP drops the connection
with a MAC checksum error. New Step 9 in Phase 3 explicitly adds
xrdp to ssl-cert and normalises the key's ownership/mode. Step
list in the help text renumbered to match.
### Aurora default when [theme] is absent
src/main.rs resolved the theme via ThemeConfig::resolve() only when
config.theme was Some(..), and otherwise fell through to a hardcoded
("dark", builtin_presets()[0].1.clone()) pair. That meant aurora
only kicked in when a [theme] section was present in config.toml,
even an empty one. ThemeConfig now derives Default (all fields
Option<String>, so zero-cost), and main.rs resolves via
config.theme.clone().unwrap_or_default().resolve() so absent and
empty [theme] behave identically. Regression test added.
### docs/reverse-proxies.md
New doc covering nginx, Caddy, Apache, and Traefik with per-proxy
configs and the %2F-decoding gotcha that causes 404s on nested
subfolder paths when a proxy normalises the URI before forwarding.
The issue was surfaced by @mauroparente in #105 — thanks for the
repro and the nginx config that made the root cause obvious.
deployment-guide.md Step 3 and integrations.md HAProxy section
both link to the new doc.
### RDP NTLM default
New [rdp] config section with default_auth_pkg. The resolver in
session.rs walks entry value -> config default -> hardcoded "ntlm".
Kerberos/Negotiate are still selectable per-entry or via the config
override, but the default is NTLM because Kerberos needs a KDC
reachable via DNS (often over TCP) and its failure mode is a silent
RDP hang that looks exactly like an unrelated network issue.
Existing entries and Guacamole-imported entries that stored
auth_pkg = None now resolve to NTLM automatically. Admins who do
run Kerberos-integrated hosts can set default_auth_pkg =
"kerberos" or "negotiate" in config.toml to restore the old
behaviour.
UI: the entry modal's NLA dropdown now says "Server default (NTLM)"
instead of "Default (negotiate)" so the behaviour matches the
label. Added an explicit "Negotiate (Kerberos first, NTLM fallback)"
option for completeness.
5 unit tests cover the resolver matrix (entry wins, empty entry
falls through, no entry falls through, empty config default falls
through, server default wins when entry is None).
### Connections tree persistence
Folder expansion state and the selected folder are now persisted
to localStorage, so reopening the page / logging back in no longer
collapses the whole tree or snaps you back to the alphabetical
first folder.
- `rustguac_connections_expanded`: {scope|path: true} map, saved
on every toggleFolder() and on the auto-expand-on-subfolder-
create path.
- `rustguac_connections_selected`: {scope, path}, saved on every
selectedFolder assignment (click, new folder, new subfolder,
delete-to-null, move entry).
On page load, loadFolders() now chains: fetch top-level folders ->
restoreExpandedTree() walks saved keys shallowest-first so deeper
paths can resolve via findFolder() after their ancestors populate
subfolderCache -> try restoring saved selection -> fall back to
the current auto-select-first behaviour only if nothing restored.
Stale keys (deleted folders, ACL-revoked folders) are dropped
opportunistically during the restore walk. Per-browser, not
per-user; try/catch wraps every storage call so private-mode / quota
errors degrade silently to the pre-persistence behaviour.
New SQLite table addressbook_audit_log records destructive and
mutating actions against the connections tree: create_folder,
update_folder, delete_folder, create_entry, update_entry,
delete_entry. Each row captures user_email, action, scope,
folder_path, optional entry_name, client IP, a small JSON details
blob, and created_at.
Details is deliberately headline-only: counts for delete_folder
(subfolders_deleted, entries_deleted), allowed_groups_count +
inherit_from_parent for folder writes, session type for entry
writes. Entry field values (password, private_key, hostname) and
full request bodies are never written. Audit rows live in SQLite,
not Vault, so logging content would leak Vault-only secrets onto
disk.
The six handlers in api.rs gain ConnectInfo + HeaderMap +
TrustedProxies + Db extractors so client IP can be resolved
through the existing auth::client_ip helper (respecting trusted
proxies) and the audit row can be written post-success.
New admin-only endpoint GET /api/admin/addressbook-audit mirrors
the existing token audit shape (limit + email filter, capped at
1000). admin.html gains a "Connections Audit Log" section below
the token audit, auto-loaded on page show.
cleanup_old_audit_log now also sweeps the new table on the same
retention window as token_audit_log.
Prior behaviour: delete_folder only cleared entries and .config of
the named folder; subfolder .config keys were left behind. Pre-v1.6.0
that was fine because subfolders didn't exist. Post-subfolders the
UI would refresh after delete, list_children would still find the
orphaned subfolder markers, and the folder would appear stuck (the
DELETE request returned 204, no error surfaced, nothing looked
broken except the folder refused to go away).
delete_folder now BFS-collects every folder path in the subtree
before wiping entries and .config at each level. Return type changes
from Result<(), _> to Result<(usize, usize), _> so the endpoint can
report (subfolders_deleted, entries_deleted) back to the UI; the
single caller in api.rs is updated.
UI: confirmation prompt now explicitly mentions "AND all its
subfolders and entries" when the selected folder has_children is
true, so admins don't nuke a subtree by accident. After a successful
delete, a transient banner reports the number of entries (and
subfolders when > 0) that got swept up.
FolderConfig gains `inherit_from_parent: bool` (default false via
#[serde(default)] so existing deployments keep their per-folder-only
semantics). When true, if a folder's own allowed_groups doesn't grant
the caller, the access check walks up the slash-separated path and
evaluates each ancestor the same way. Admins still bypass all checks.
Inheritance stops at any folder with the flag off, preserving the
ability to lock down a specific subtree.
New resolve_folder_access helper in vault.rs centralises the walk-up
logic; check_folder_access, ab_list_folders, and ab_list_all in
api.rs all route through it so access semantics live in one place.
Import: new --allowed-groups flag writes the ACL onto the root import
folder only; subfolders are created with inherit_from_parent=true so
the whole imported tree picks up the same rules without per-folder
writes. Matches the pattern admins will want after an initial
Guacamole migration where every connection group should be visible
to the same OIDC group.
Related fix: list_credential_variables in api.rs no longer scans only
top-level folders — it now recurses into subfolders so variable
references buried in an imported tree show up on the My Credentials
page (previously they stayed invisible post-v1.6.0 when subfolders
became first-class).
UI: folder modal gets an "Inherit permissions from parent folder"
checkbox. Defaults: unchecked for new top-level folders (no parent
to inherit from), checked for new subfolders, and reflects the
stored value when editing.
- db: auto-repair db file ownership when init_db runs as root against a
data dir owned by a non-root user. Fixes "attempt to write a readonly
database" after operators run `rustguac add-admin` under sudo, which
previously left the sqlite file owned by root:root and unwritable by
the rustguac service user.
- import-guacamole: preserve Guacamole connection-group hierarchy as
real Vault subfolders instead of flattening into hyphen-joined entry
names. Each ancestor group gets its own .config so empty parents
still render in the Connections tree. Per-folder dedup so the same
connection name in two groups no longer collides.
- config: fatal, loud, line-annotated error on malformed config.toml.
Previously a TOML parse error emitted a tracing::warn before the
subscriber was initialised and silently fell back to built-in
defaults, leaving the operator debugging a "working" server that
ignored their entire file. Errors now print via eprintln with the
toml crate's column-pointed snippet and exit(1) whenever a config
path was explicitly given (or /opt/rustguac/config.toml exists).
Re-captured from sol1-remoteconsole running 1.6.1 with the default
aurora theme and the new Connections layout (renamed from Address
Book in v1.6.0). User-identifying data sanitised; a temporary demo
folder with clearly-fake entries (example.com, saucedemo, internal
bastion hops) was seeded and torn down around the capture.
New views added:
- connections.png — subfolder tree + scope icons, all five entry
type badges
- vdi_connection.png — Docker VDI editor showing container image,
CPU/memory limits, allow_sharing + auto_open_if_singleton toggles
- reports_view.png — aggregate counters + filterable session history
Refreshed:
- rdp_connection.png — NLA auth pkg, KDC URL, RemoteApp, recording
- web_connection.png — banner, autofill with $USERNAME/$PASSWORD,
allowed domains
- ssh-tunnel.png — multi-hop flow diagram with jump host cards
- sessions_view_with_adhoc.png — collapsible ad-hoc form + active
sessions panel
- recordings_view.png, recordings_player_histogram.png
- admin_console.png — v1.6.1 badge, system-status cards, users table
- tokens_view.png — group-to-role mappings + user API tokens
Removed screenshots/address_book.png (renamed to connections.png
as per the v1.6.0 rework).
Security audit (v1.6.1 cycle) findings:
- Shadow tokens: each use now writes a shadow_used entry to
token_audit_log with the connecting IP. Prior behaviour audited
only the mint, so a leaked token could be replayed within its
10-minute TTL with no observable record.
- OIDC groups claim: bound per-name length to 256 bytes (UTF-8
boundary preserved) and array size to 64. A misconfigured or
compromised IdP could previously bloat the seen_groups table
unbounded.
- .cargo/audit.toml formalises the rsa (RUSTSEC-2023-0071) and
rustls-pemfile (RUSTSEC-2025-0134) advisory ignores with
rationale, replacing the inline --ignore flag in CI. New
advisories on those crates will still surface.
Small defence-in-depth: WebSocket Origin/Host compare is now
case-insensitive (DNS is canonical) — previously exact-match.
Test harness grows from 150 to 215 tests:
- Pure-function regression coverage: OIDC groups cap, shadow-token
validation matrix (owner/shadow/invalid/expired/IDOR), Vault
path traversal, VDI username sanitization, recording-name
hardening, JPEG magic bytes, Guacamole protocol parser
adversarial cases (overflow length, UTF-8 split, malformed
frames, buffer cap, streaming boundaries), Origin/Host matcher.
- Async + in-memory state: SessionManager test helper bypassing
disk/browser, owner/shadow validation end-to-end, mint prunes
expired, shadow is session-scoped (IDOR guard), disconnect_viewer
saturating decrement, seen_groups DB bounds, rate-limit layer
burst -> 429 (proves tower_governor is actually applied).
Behaviour-preserving refactors to support testing:
check_share_token_match (from validate_share_token),
is_jpeg_magic (from thumbnail PUT), origin_host_matches (from
ws_handler CSWSH check).
Docker pipeline
- Multi-arch build on ubuntu-latest with QEMU was the long pole of
the release pipeline (arm64 took 30–60 min vs ~5 min for the
native-arm .deb job). Split into three jobs:
- build-docker-amd64 on ubuntu-latest
- build-docker-arm64 on ubuntu-24.04-arm (same native runner the
arm64 .deb build uses)
- build-docker-manifest combines them with
`docker buildx imagetools create` into the consumer-facing
`sol1/rustguac:VER` and `:latest` multi-arch manifest lists.
- Consumer-facing tags are unchanged — `docker pull
sol1/rustguac:latest` still auto-picks the right arch. The per-arch
intermediate tags (`:VER-amd64`, `:VER-arm64`) appear as byproducts
on Docker Hub but aren't required.
- The release job now depends on build-docker-manifest instead of
the removed build-docker.
Dependency refresh (closes 5 low-severity Dependabot alerts)
- `cargo update` at root and in fuzz/:
- rustls-webpki: fuzz/ was 0.103.x < 0.103.12 → now 0.103.12
(main lock was already there from v1.5.5)
- rand 0.9.x: < 0.9.3 → 0.9.4 (GHSA-cq8v-f236-94qc: unsound with
a custom logger inside rand::rng())
- rand 0.10.0 → 0.10.1 (same GHSA)
- We don't hook loggers into rand so the unsoundness never triggered
in practice, but getting to clean alerts is worth a lockfile bump.
Feature #103: single-entry auto-connect
- New `auto_open_if_singleton: Option<bool>` on AddressBookEntry and
EntryInfo. Admin ticks it per entry in the Connections modal;
importer initialises to None.
- After the /api/addressbook batch resolves on the Connections page,
if the user sees exactly one entry and it has the flag set, the
page fetches /connect and navigates the current tab to the new
session. Same-tab navigation (not a popup) because browsers block
window.open after an async fetch without a user gesture.
- A sessionStorage flag gates the auto-open to once per browser
session — an accidental refresh of Connections doesn't re-spawn
sessions. Logout clears the flag so the next login fires again.
- Admins never meet the singleton condition (they have many
entries) so this is effectively kiosk-only.
Escape hatch: client.html Home button
- Ctrl+Alt+Shift panel header now has a 🏠 Home button next to the
close ×. Takes the tab back to /connections.html — primary route
for a kiosk user whose session auto-opened into this tab.
Bug fix: folder-modal group picker silently added a group on save
- addFmGroupFromInput used to fall through to picking the combo's
highlighted suggestion when the input was empty. fm-save calls
addFmGroupFromInput to flush any typed-but-not-chipped text; with
an empty input that meant every Save on an existing folder tacked
on the first unselected known group (e.g. an operator group) —
visible only on the next edit. Split the "pick suggestion when
empty" behaviour out to an explicit helper that only fires from
Enter-with-empty-input, never from Save.
Per-entry Share gating
- AddressBookEntry gains an `allow_sharing: Option<bool>` (default
None = off). Admin opts in per entry via an Allow session sharing
checkbox in the entry modal.
- EntryInfo exposes the field so the modal prefills on edit.
- CreateSessionRequest carries it through; the ab_connect_entry
quick-connect and legacy quick-connect paths propagate the entry's
value.
- Session gets a share_allowed bool. SessionInfo.share_url is only
populated when share_allowed is true, so the Connections
Active Sessions Share button auto-hides without any new UI logic.
- Default behaviour on create:
* explicit allow_sharing on the request → honoured
* entry-derived session without flag → off (admin opt-in)
* ad-hoc session (no address_book_entry) → on
The ad-hoc default preserves the long-standing API-key
session-creation flow where external callers expect share_url in
the POST /api/sessions response.
Modal checkbox alignment (side-effect fix)
- The generic .modal input rule in rustguac.css was forcing every
input — checkboxes included — to 44px height + block + 100% width,
which misaligned every tickbox/label pair across the entry, folder,
and onboarding modals.
- Added carve-outs for input[type=checkbox] / [type=radio] that keep
native size, inline, with a small right margin.
- Labels that directly contain a checkbox/radio (via :has()) now flex
the control + text on one baseline with a clean gap, and drop the
uppercase letter-spacing used for full-width field labels.
Modified-stage-4: always-visible buttons, no hover-reveal.
- Each active session card now has a Terminate button next to Share.
Confirmation dialog on click, DELETE /api/sessions/{id}, reloads
the grid on success. Hover state uses .btn-danger (primary red).
- Share button still hides when share_url is absent so the upcoming
per-entry allow_sharing toggle gates it automatically.
- Share modal's caution text was a dim .field-hint that was easy to
miss. It now renders as a proper warning banner: 1px border +
bumped left edge in --status-pending (theme-aware yellow), ⚠ icon,
and larger body text for legibility.
Polish for the Sessions page now that it's the admin/poweruser
management view (share has moved to Connections, shadow landed in
stage 2).
- Page heading becomes a .section-head strip with a right-aligned
count span ("12 active · 3 yours" for admins seeing others,
"3 active" when the caller only sees their own).
- Empty state is role-aware: admins get the plain "No active
sessions", non-admins get a nudge toward Connections / the ad-hoc
form.
- Owner column is dimmed for own sessions and accent-teal bold for
others — lets admins eye-scan own vs others at a glance.
- Delete button is hidden on rows the caller can't delete (non-admin
viewing someone else's session). Backend check is still the
authoritative gate; this just tidies the UI.
Connections
- Clicking an active session card used to POST /connect for entries
with an address_book_entry, which minted a new session every time.
Confirmed in the log: a web-session owner clicked back to their
active card after a shadow join and got two duplicate sessions.
- Now the click just opens s.client_url — which attaches to the
existing session. Dormant VDI containers below still need /connect
(no live session to attach to), so that branch is untouched.
Tokens page
- Operator/viewer roles saw the Sessions nav link on the Tokens page
(every other page correctly hid it for level < 3). Clicking it
redirected straight back out because sessions.html rejects level < 3.
- Added the same hide-on-level-<3 logic as the other pages; also
reveal the Reports link for level >= 3 in the same pass.
Part of the shadow-sessions plan (stage 2). Admins can now join any
active session from the Sessions page without the user having to
share — the backend mints a short-lived token and every mint is
logged to token_audit_log.
Backend
- Session struct gets a shadow_tokens: Vec<ShadowToken> (sha256 hex
of the raw token, issuing admin, expiry). Expired entries are
pruned on mint.
- validate_share_token now accepts either the owner's share_token or
any non-expired shadow token, so the existing viewer path works
unchanged.
- POST /api/sessions/{id}/shadow (admin-only) mints a raw token,
pushes its hash + a 10-minute expiry onto the session, writes a
token_audit_log row (action = "shadow_session", details includes
session_id, owner, expiry, caller IP) and returns the viewer URL.
Frontend (Sessions page)
- /api/me fetched on load so we know the caller's display_name and
role before rendering. API-key users default to admin and fetch
/api/me to learn their name.
- Own active sessions still render "open" (anchor to client_url).
- Others' active sessions render "shadow" as an anchor (not a button)
so both action cells line up in the same column. Shadow uses
--status-pending (warning yellow) instead of the neutral accent,
hovers to --primary, and shows a "minting..." busy state while
the POST is in flight.
Part of the Sessions-page rework (shadow-sessions plan, stage 1).
The Sessions page is becoming an admin/poweruser management view;
user-initiated share lives with the user's own active sessions in
Connections.
Connections
- Each active session card gets a Share button in a new action row
below the thumbnail/meta.
- Share opens a themed modal with the full share URL pre-selected
and a Copy button (async clipboard with execCommand fallback).
- Clicking the card still reconnects — Share handler stops
propagation so the two actions don't collide.
- Overlay click or Close dismisses the modal.
Sessions page
- Share column removed (was columns 10 of 10 — down to 9).
- Dead CSS (.btn-share, .share-url) and JS (expandedShares,
shareFullUrl, rowId, the share/copy click handlers) removed.
- Ad-hoc jump-host styling (.btn-add-hop) stays.
Three pieces of v1.6.0 work that happened together and are easier to
review as one save point.
Rename: Address Book → Connections
- static/addressbook.html renamed to static/connections.html
- Nav links, page titles, empty states, onboarding, and prose updated
across all 8 static pages (connections, admin, docs, index,
recordings, reports, sessions, tokens).
- README, CLAUDE.md, and every file under docs/ updated.
- src/main.rs: connections.html added to the branded-page map and
route list; /addressbook.html returns a 308 permanent redirect so
existing bookmarks keep working.
- Backend API paths, Rust types, and Vault storage paths are
deliberately unchanged — internal only.
Folder allowed_groups picker
- New SQLite table `seen_groups` tracks OIDC groups observed in any
user login; OIDC callback upserts after extracting groups.
- `GET /api/auth/known-groups` (admin-only) returns the union of
group_role_mappings and seen_groups.
- `GET /api/addressbook/folders/{scope}/{folder}/config` adds the
missing endpoint the frontend was already calling — existing
allowed_groups now prefill the edit-folder modal.
- Folder modal swaps the free-text comma-separated input for a chip
picker with a themed combobox dropdown: autocomplete over known
groups, keyboard nav, "+ add custom" row for unlisted groups.
Active session visibility (GitHub #102)
- `GET /api/sessions` scopes to the caller's own sessions by default;
`?all=true` lets admins opt in (used by the Sessions page).
- `GET /api/sessions/{id}` and the thumbnail GET/PUT endpoints are
now owner-or-admin, returning 404 for other callers so session
existence isn't leaked.
- Connections' Active Sessions strip is now always owner-scoped —
admins still manage everyone via the Sessions page.
Frontend for #101 — subfolder support (backend landed in c2a3822) — plus
a site-wide visual overhaul extracted into a single shared stylesheet.
Address book
- Folder sidebar renders as a tree with lazy-loaded children via the
/api/addressbook/folders/{scope}/{path}/subfolders endpoint.
- Scope badge is now an icon with hover tooltip: ⊕ shared, ▣ instance.
- New "+ subfolder" button creates a nested folder under the selection.
- Move-entry dropdown includes any loaded subfolders.
- Batch folder API now returns path + has_children so the tree can
render chevrons without a second request per folder.
Design system (rustguac.css)
- Extracted ~700 lines of near-duplicate CSS from each page into a
shared stylesheet linked by every page.
- 18px body, strict 38/44/54px control heights, uppercase letter-spaced
section labels, zebra table rows, active-nav underline bar.
- Uniform button ladder: primary (red) / accent (connect, teal) /
ghost (+ buttons) / small (edit/delete chrome).
- Generic status colors, type badges, pagination, token-reveal,
summary cards, hop cards, and flow diagram now live in one place.
Per-page updates
- addressbook, admin, docs, index, recordings, reports, sessions,
tokens: style blocks reduced to page-specific layouts only.
- reports/recordings/sessions: bare <strong> page titles promoted
to <h2> for proper heading hierarchy.
- Stripped inline padding/font-size attributes that were overriding
the shared ladder.
Add hierarchical folder support to the Vault-backed address book.
Folders can now be nested (e.g., Clients/Acme/Servers) using Vault
KV v2's natural path hierarchy.
- Add validate_path() for multi-segment folder paths, replacing
validate_name() for folder parameters. Each segment validated
individually — blocks traversal, reserved names, special chars.
- FolderInfo gains path and has_children fields for tree UI support
- New list_subfolders() and list_children() methods on VaultClient
- New GET /api/addressbook/folders/{scope}/{folder}/subfolders endpoint
- Existing flat folder operations unchanged (backward compatible)
- Client percent-encodes folder paths: Clients%2FAcme in URL decodes
to Clients/Acme — no wildcard routes needed
Tested on sol1-remoteconsole: subfolder CRUD, entry CRUD in subfolders,
has_children detection, and existing flat folder compatibility verified.
- Switch default theme from dark to aurora across server config,
all 9 static HTML pages, example config, and docs
- Fix theme docs: list all 8 presets (was 6, missing jaguar/aurora),
add missing type_vdi_bg/type_vdi_fg fields
- Add Vault/address book setup as recommended post-install step in
installation docs — the address book is the primary user-facing
feature and requires Vault
- Renumber subsequent install steps
- Add max_sessions (default 500) and max_sessions_per_user (default 50)
config options. Session creation is rejected with a clear error when
limits are reached. Set to 0 for unlimited (backward compatible).
- Add background reaper that removes completed/error/expired sessions
from the in-memory HashMap after session_cleanup_delay_secs (default
300s). Session history in SQLite is not affected.
- Prevents resource exhaustion from unbounded session creation and
memory leak from accumulated completed sessions.
Closes#99
Avoids false rejections behind reverse proxies where the Host header
may include an explicit port (e.g. :443) that the browser's Origin
omits as a default port.
From OWASP-based security audit (categories 2-12):
- Vault path traversal: validate folder names on read operations
(get_folder_config, list_entries, get_entry) — write operations
already validated but reads did not
- SSH host key: reject connection when stored key fails to parse,
instead of silently accepting (was bypassing verification)
- Config secret redaction: custom Debug impls for OidcConfig and
VaultConfig that redact client_secret, role_id, and client_key
- Branding XSS: HTML-escape site_title and logo_url config values
before injecting into page templates
- WebSocket CSWSH: validate Origin header against Host header on
WebSocket upgrade, reject cross-origin requests
- VDI bind mounts: add nosuid,nodev mount options to home directory
bind mounts to prevent setuid binary attacks
- Recording access: restrict list/serve endpoints to poweruser+ role
(previously any authenticated user including viewers could access
all recordings)
- Redact share_url from session listings for non-owners — previously
any authenticated user could enumerate share tokens and join sessions
they didn't create (share_url now only returned to session creator
and admins)
- Always rate-limit OIDC login/callback (1/sec burst 5 per IP)
regardless of rate_limit config, preventing brute-force on auth
- Fix disconnect instruction detection: use instruction boundary
matching instead of substring contains — clipboard content or typed
text containing "10.disconnect;" could falsely trigger VDI container
destruction
- Restrict sudoers chown rule to rustguac:rustguac only, preventing
arbitrary ownership changes on the LUKS mount point
Bollard 0.20 moved container option types from bollard::container to
bollard::query_parameters, replaced Config<T> with ContainerCreateBody,
and changed several fields to Option types. Update all imports and call
sites in the VDI Docker driver accordingly.