Commit Graph

28 Commits

Author SHA1 Message Date
Dave Kempe e79883224f RDP: default to NTLM + persist Connections tree state
### 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.
2026-04-22 16:32:37 +10:00
Dave Kempe 9dbdc17843 Security hardening + test harness expansion
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).
2026-04-19 14:46:07 +10:00
Dave Kempe a0c582dd45 cargo fmt 2026-04-18 22:08:08 +10:00
Dave Kempe 3b97dfbcdf Per-entry allow_sharing toggle + modal checkbox alignment fix
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.
2026-04-18 21:56:20 +10:00
Dave Kempe 635bc061cc Shadow sessions: admin read-only viewer tokens
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.
2026-04-18 21:56:20 +10:00
Dave Kempe 84616f5730 Session limits and completed session cleanup (#99)
- 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
2026-04-12 07:40:32 +10:00
Dave Kempe 622569290a Security hardening: share URL redaction, auth rate limiting, fixes
- 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
2026-04-11 21:49:03 +10:00
Dave Kempe 958b675fd2 SSH tunnel: host key verification with UI-driven pinning
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
2026-04-09 21:22:36 +10:00
Dave Kempe 75eae2b43f cargo fmt 2026-04-05 10:33:01 +10:00
Dave Kempe a9a966e675 VDI: detect image change and clear stale thumbnails
- 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.
2026-04-05 08:33:42 +10:00
Dave Kempe 3273e47bbc VDI: per-entry idle timeout for 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.
2026-04-05 07:55:50 +10:00
Dave Kempe 697b6f7d75 Active Sessions with thumbnails, dormant VDI 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)
2026-04-05 07:51:00 +10:00
Dave Kempe 690997d4eb VDI: persistent home dirs and logout stops container
- Add home_base config: bind-mounts {home_base}/{username} into container
  so user files persist across container restarts
- When guacd ends the connection (user logout / session crash), stop and
  remove the VDI container immediately
- Browser disconnect (tab close, network drop) keeps container running
  for reconnection — idle reaper handles eventual cleanup
2026-04-05 06:44:33 +10:00
Dave Kempe 501bd14a0d VDI Docker driver: ephemeral container desktops
Add 5th session type (Vdi) that spawns Docker containers with xrdp
and connects via RDP through guacd. Containers persist after disconnect
for configurable idle timeout, allowing reconnection to same desktop.

- VdiDriver trait (dyn-compatible) for downstream extensibility
- DockerDriver: bollard API, deterministic container naming per user,
  start/reuse/stop lifecycle, TCP+settle readiness polling
- Background reaper removes idle containers after timeout
- Address book: container_image, cpu/mem limits, env vars fields
- Config: [vdi] section with enabled, docker_socket, limits, timeouts
- Test image: contrib/vdi-test-image (Debian trixie + xrdp + xfce4)
2026-04-05 06:28:03 +10:00
Dave Kempe 8e9edce3a0 Security hardening: 8 fixes from audit
- Recording name: reject on canonicalization failure (not accept)
- Protocol parser: 1MB buffer limit prevents OOM from rogue guacd
- UTF-8 validation: reject invalid bytes instead of silent replacement
- Login scripts: remove credential env vars (stdin-only)
- Chromium profiles: 0700 permissions on temp directories
- Share tokens: constant-time comparison (subtle::ConstantTimeEq)
- WebSocket: drop binary messages instead of forwarding to guacd
- Login scripts API: require operator+ role
2026-03-28 22:28:30 +11:00
Dave Kempe 799cdefd57 v1.0.0: H.264 passthrough, per-entry toggle, Docker fix
H.264 passthrough for RDP via guac_display worker integration:
- Raw H.264 NAL units from xrdp x264 pass through to browser
  WebCodecs VideoDecoder, bypassing server-side decode/re-encode
- Frame queue preserves all H.264 frames across deferred flushes
- Per-entry toggle in address book (enable_h264 field)
- Keyframe gate ensures decoder initializes correctly
- Per-connection callback (no static global concurrency bug)
- Queue capped at 120 frames to prevent unbounded growth

guacd patch (004-h264-display-worker):
- display-priv.h: h264 frame queue on guac_display_layer
- display-plan.c: flush h264 queue during plan_apply, skip IMG ops
- display-layer.c: set_h264 API with queue append and cap
- rdpgfx.c: SurfaceCommand wrapper saves NAL before GDI handler
- settings.c/h: enable-h264 connection parameter, conditional GfxH264

Other changes:
- Docker: fix FreeRDP plugin path for drive/audio channels (#87)
- contrib/setup-xrdp-gfx.sh: full xrdp x264 rebuild from Debian sid
- docs/rdp-video-performance.md: H.264 passthrough documentation
- Address book UI: H.264 checkbox with dependency descriptions
2026-03-28 21:52:28 +11:00
Dave Kempe 3bf1762d87 v0.9.0: RDP audio, GFX pipeline, video performance
Audio:
- RDP audio output now works through guacamole. Advertise audio/L16
  and audio/L8 mimetypes in the guacd handshake, and explicitly set
  disable-audio=false. Fixed mimetype mismatch that silently prevented
  guacd from creating audio streams.
- Browser AudioContext auto-resumed on user interaction (click/keydown)
  to comply with autoplay policy.

Video performance:
- Per-entry GFX pipeline toggle (enable_gfx) — enables RemoteFX codec
- Per-entry desktop composition toggle (enable_desktop_composition)
- Per-entry force lossless toggle (force_lossless) — PNG-only mode
- WebSocket proxy buffer increased from 8KB to 64KB
- Video Performance section in address book UI for RDP entries

Documentation:
- RDP Video Performance guide with Windows server tuning (AVC444,
  60fps, GPU encoding) and Linux xrdp setup (Debian 13)
- contrib/setup-xrdp-gfx.sh — automated GFX/H.264 setup for xrdp
- contrib/setup-xrdp-audio.sh — automated PulseAudio module build
2026-03-24 20:25:05 +11:00
Dave Kempe 7acbb302fa v0.8.5: Reports, session history, enriched recordings
Session history:
- New session_history SQLite table tracks all sessions with user,
  entry, folder, hostname, duration, and recording file
- Automatic cleanup via session_history_retention_days config (default 90)
- 8 new unit tests for session history DB functions

Reports page (poweruser+ only):
- Summary cards: total sessions, hours, unique users, active now
- Sortable/filterable session history table with pagination
- Top Connections and Top Users leaderboards
- CSV export for session history with filters

Enriched recordings:
- Recording .meta sidecars now include user, folder, entry name, session type
- Recordings API returns enriched metadata from .meta files
- Recordings page shows User, Entry, Folder columns

Other:
- Reports nav link on all pages, hidden for operator/viewer roles
- Reports documentation in docs/reports.md
- Onboarding wizard for new users (role-scoped, dismissable)
- Settings menu label visibility improvement
- Fixed stale credential variable test (hyphen support)
2026-03-22 20:36:26 +11:00
Dave Kempe 6bafc79d60 v0.8.0: Credential variables, bug fixes
Credential variables — address book entries reference $domain_username /
$domain_password instead of storing static credentials. Users fill in their
own values via My Credentials (gear menu), stored per-user in Vault KV.
All variables set → silent launch; missing → prompted. Hyphens allowed in
variable names. Docs section added.

Bug fixes:
- Rate limiting disabled by default; opt-in via rate_limit = true (#62)
- Docker: copy FreeRDP guac-common-svc plugins to runtime image (#64)
- Docker/install: add chromium-sandbox package for non-root web sessions (#61)
- Logo: skip redundant JS src= when server-side branding already set (#65)
- Sessions page: hide Open/Share buttons for non-active sessions (#63)
- Drive: expose drive_configured in /api/auth/status, warn in UI when
  [drive] not configured
- install.sh: verify FreeRDP plugin installation

UI polish:
- Nav bar: border separator + spacing between header and nav on all pages
- Address book: password show/hide toggle on all password fields
- Drive diagnostic logging (session.rs, websocket.rs, client.html)

Closes #61, #62, #63, #64, #65
2026-03-13 13:47:11 +11:00
Dave Kempe d07b8ae225 v0.7.0: Login script dropdown, batch address book, Docker non-root
New features:
- Login script selector: dropdown populated from server scripts dir (#52)
- Batch address book endpoint eliminates N+1 API calls (#56)
- Clone button for address book entries (#56)
- Increased API rate limits (#56)

Fixes:
- Docker container runs as non-root user (#50)
- Conditional --no-sandbox when running as root (#50)
- Post-spawn Chromium liveness check with stderr capture (#50)

Docs:
- Theme/branding configuration guide (#55)
- Vault metadata policy for deletes (#54)
- TLS config clarification (no boolean toggle)
2026-03-11 21:54:38 +11:00
Dave Kempe ac77bfab98 v0.6.1: Vault mTLS support, comprehensive test suite (87 tests)
Vault/OpenBao mTLS:
- Add ca_cert, client_cert, client_key fields to [vault] config
- Supports custom CA certificates and mutual TLS authentication
- Clear error messages for missing files, invalid PEM, missing key
- Fixes #48 (OpenBao requiring client certificates)

Test suite (8 → 87 tests):
- vault: 13 tests (TLS client builder, config deserialization, name validation)
- auth: 11 tests (role hierarchy, effective role capping, XFF/trusted proxy, has_role)
- session: 12 tests (CIDR network checks, autofill JSON parsing, placeholder substitution)
- browser: 8 tests (Chromium password encryption, Login Data SQLite, RangeAllocator)
- config: 8 tests (preset resolution, theme overrides, defaults, vault config)
- api: 6 tests (HTML escaping, recording name path traversal protection)
- db: 7 tests (SHA-256 hashing, key generation, user groups parsing)
- import: 12 tests (already existed, now wired into module tree)
- protocol: 8 tests (already existed)

Other:
- Wire import.rs into module tree (fixes orphaned tests)
- Document mTLS config in configuration.md and integrations.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:44:46 +11:00
Dave Kempe 99d79fe05d v0.6.0: Web autofill, domain allowlisting, clipboard control, Guacamole import
New features:
- Native Chromium autofill: pre-populate Login Data SQLite before launch,
  zero external deps (no Node.js/Playwright needed for simple login flows)
- Per-entry domain allowlisting: restrict which domains Chromium can reach
  via --host-rules (separate from server-side web_allowed_networks CIDR)
- Per-entry clipboard control: disable-copy and disable-paste for all
  session types (SSH, RDP, VNC, Web) via guacd native parameters
- Guacamole import: `rustguac import-guacamole` parses mysqldump SQL and
  writes entries to Vault address book

Security hardening:
- Comprehensive Chromium managed policy deployed via install.sh, Dockerfile,
  and debian/postinst (blocks DevTools, downloads, file dialogs, extensions,
  dangerous URL schemes)
- Profile isolation: each web session gets a unique UUID-based profile dir
- Autofill credentials encrypted with Chromium's native os_crypt (AES-128-CBC)

Documentation:
- Updated README, docs/api.md, docs/security.md, docs/configuration.md,
  docs/overview.md, docs/integrations.md with all new features
- Clarified two-layer domain restriction (web_allowed_networks vs allowed_domains)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:54:26 +11:00
Dave Kempe ea72c52a31 v0.5.0: Theme system, ARM64 builds, Docker fixes, dependency updates
Theme system:
- 6 built-in presets (dark, light, high-contrast, terminal, nord, corporate)
- Admin configures preset + per-color overrides in [theme] config section
- Client-side theme switching via localStorage (flash-free)
- All static pages updated with 28 CSS custom properties

Proxy telemetry:
- Track which side terminated connection (guacd/browser/cancelled)
- Timing-based log levels (guacd close <5s = warning)
- Clamp session dimensions to safe ranges (width 640-8192, height 480-8192, DPI 16-384)

Docker fixes (#37):
- Fix port mismatch: Dockerfile now uses 8089 consistently
- Auto-generate admin API key on first run
- Add API key setup docs and recordings volume to compose example

ARM64 support:
- Multi-platform Docker builds (linux/amd64 + linux/arm64)
- Native ARM64 .deb and tarball builds via ubuntu-24.04-arm runner

Dependency updates:
- rustls 0.23.37, chrono 0.4.44, clap 4.5.60, toml 1.0.3
- futures-util 0.3.32, uuid 1.21.0, pulldown-cmark 0.13.1
- actions/upload-artifact v7, actions/download-artifact v8

Also: FreeRDP 3.x NULL deref patch (003), .gitignore for .playwright-mcp/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:05:41 +11:00
Dave Kempe 3cb8111591 Add RDP RemoteApp/RAIL, recording rotation, per-entry recording overrides
RemoteApp/RAIL (closes #19):
- Pass remote-app, remote-app-dir, remote-app-args through to guacd
- Address book UI: collapsible RemoteApp section for RDP entries

Recording rotation:
- New [recording] config section (backwards-compatible with recording_path)
- Automatic disk-space management: max_disk_percent, max_recordings thresholds
- Background rotation task with configurable interval
- Sidecar .meta JSON files track address book entry per recording

Per-entry recording overrides:
- Address book entries can enable/disable recording and set max recordings
- Per-entry rotation runs on session disconnect
- UI: collapsible Recording Settings section for all connection types

Bump version to 0.4.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:39:25 +11:00
Dave Kempe 76c93bc502 Add multi-hop SSH tunnels, VNC sessions, and web session tunnel support
Multi-hop SSH tunnel chains allow routing any session type through
multiple bastion hosts. VNC is now a first-class session type.
Web browser sessions can tunnel through jump hosts with automatic
URL rewriting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 08:22:34 +11:00
Dave Kempe d07d79c4f2 Add Kerberos NLA support for RDP and credential prompting
- Patch guacd with Kerberos NLA support (002-kerberos-nla.patch),
  based on upstream GUACAMOLE-2057 PR #581, adapted for FreeRDP 3.x
- Add per-entry auth_pkg, kdc_url, and prompt_credentials settings
  to the address book (configurable in admin UI)
- Frontend credential prompt for entries without stored credentials
  or with prompt_credentials enabled (never stored, session-only)
- Wire auth-pkg, kdc-url, kerberos-cache params through rustguac
  to the guacd RDP handshake
- Comprehensive Kerberos NLA docs: krb5.conf setup, KDC discovery
  options, FQDN requirements, troubleshooting guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 10:39:18 +11:00
Dave Kempe 2187cfc1e4 Security hardening: 5 fixes from audit
- Vault TLS: replace hardcoded danger_accept_invalid_certs(true) with
  configurable tls_skip_verify option (default: false)
- Share tokens: use constant-time SHA-256 hash comparison to prevent
  timing side-channel attacks
- OIDC pending states: add 10-minute TTL, evict stale entries on each
  login to prevent unbounded HashMap growth
- Recording path traversal: add canonical path validation as defense-
  in-depth alongside existing string checks
- Frontend XSS: escape all user-controlled data (filenames, paths) in
  innerHTML via escapeHtml/escapeAttr in client.html and recordings.html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 11:34:54 +11:00
Dave Kempe 67101e27ce Initial public release
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 14:38:53 +11:00