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.
check_server_key no longer blindly accepts all keys. Jump hosts now
support a host_key field stored in Vault alongside credentials.
- New POST /api/ssh/probe-host-key endpoint probes an SSH server and
returns its public key, fingerprint, and algorithm
- Address book UI: "Verify Host Key" button per jump host probes the
server, shows fingerprint for confirmation, stores key on save
- TunnelHandler verifies the server key against the stored key on
connect — rejects with detailed error on mismatch
- Unpinned keys accepted with TOFU warning log for backward compat
- host_key preserved through credential merging on entry update
Closes#95
Frame dropping at queue depth 5 triggered during normal operation and
broke the H.264 reference chain, corrupting output until the next
keyframe. Remove frame dropping entirely — sync gating alone provides
sufficient flow control by delaying the sync response until decodes
complete, giving guacd accurate backpressure.
The H.264 decode path bypassed the Guacamole sync mechanism — the sync
response fired immediately without waiting for WebCodecs to finish
decoding. guacd thought the client was keeping up and sent at full rate,
causing the decode queue to grow without bound (30+ seconds of lag
observed over time).
Fix: gate the sync response on H.264 decode completion so guacd gets
real backpressure. Also drop delta frames when the decode queue exceeds
5 frames as a safety valve for transient overload (tab backgrounding).
- Add pending-decode tracking and per-frame position capture to
H264Decoder.js (fixes shared mutable state race)
- Add waitForPending() with 1s safety timeout for sync gating
- Add frame dropping when decodeQueueSize > 5 (never drops keyframes)
- Add stats() method for console debugging (__guac_client._h264Decoder.stats())
- Gate sync response in Client.js on H.264 decode completion
Closes#93
Switch reqwest from rustls-tls-webpki-roots to rustls-tls-native-roots
so the OS trust store is used by default — private CAs installed
system-wide now work without extra config.
Add ca_cert and tls_skip_verify options to [oidc] for environments
where the system store isn't available or for debugging TLS issues.
Improve OIDC discovery error formatting to surface the actual cause
(issuer mismatch, TLS failure, etc.) instead of opaque "Request failed".
Closes#94
- GitHub badges (CI, release, license, Docker pulls)
- VDI featured in architecture, features, and quick start
- Feature tables for session types, security, connectivity
- Removed RPM/RHEL references (build from source for others)
- Complete docs index with all current pages
- Cleaner quick start with Docker+VDI instructions
Logout link moved inside the float-right user-menu-wrapper so both
Logout and Settings appear on the far right of the nav bar. Fixed
addressbook.html Settings font size to match other pages (1.3em).
Logout is now a standalone nav link next to Settings (Logout Settings
order, right-aligned). Applied consistently across all 7 HTML pages.
Removed Logout from the settings dropdown menu.
- start_or_reuse checks if existing container's image differs from
the requested image. If so, stops the old container and creates
a new one with the new image.
- Clear stale VDI thumbnail when starting a new session, so old
screenshots don't linger from previous images/failed sessions.
- Run example with Docker socket mount and group-add for VDI
- Commented-out [vdi] section in default config template
- vdi-homes directory and volume for persistent home dirs
- No changes to default behavior — VDI stays disabled unless configured
- New Session form collapsed by default (click to expand)
- Added VDI (Docker) to session type dropdown with container image field
- Jump hosts hidden for VDI sessions (local containers)
Address book entries can now set container_idle_timeout_mins to override
the global idle_timeout_mins. Stored as a Docker label on the container
so the reaper reads it without needing session state. Reaper also cleans
up VDI thumbnails when removing idle containers.
Client captures display thumbnail every 10s (JPEG, 320px), uploads
to server. Address book shows "Active Sessions" section with thumbnail
grid — click to reconnect via address book connect flow.
- PUT/GET /api/sessions/{id}/thumbnail endpoints
- GET /api/vdi/containers — list running VDI containers for current user
- VDI container labels: rustguac.entry, rustguac.image for reconnect
- Thumbnail copied to container-keyed file on disconnect (persists)
- Dormant VDI containers shown with last thumbnail
- Cache-busting on thumbnail images for live updates
- Moved logout button out of settings dropdown
- Fixed: VDI containers endpoint in authenticated route group
- Fixed: thumbnail click uses address book connect (not stale session URL)
Show a centered overlay when the Guacamole client disconnects or
errors, instead of leaving a frozen canvas. Offers Reconnect (reload)
and Close (navigate to home) buttons. Applies to all session types.
Sniff for guacd's 10.disconnect; instruction in the proxy stream to
distinguish server-initiated disconnects (user logout, crash) from
browser-initiated disconnects (tab close, network drop).
- Logout from desktop → container stopped and removed immediately
- Tab close / network drop → container persists for reconnection
- Normal RDP/SSH/VNC sessions unaffected (flag only acted on for VDI)