Follows #319. The option moves out of the Session panel header into the
Controls panel's new Keyboard section, beside View, and is remembered in
the browser like the View choice. The lock follows the setting whenever
fullscreen is entered, and switching it while fullscreen applies at once.
Browsers without the Keyboard Lock API show it disabled with a note.
When entering full screen mode and if the check box to lock the keyboard
has been checked, all keyboard events will be sent to the client. For
example when connecting to a Windows machine Win+R would open the Run
dialog on the remote machine, not on the local.
(cherry picked from commit 3cfcc5cdd3)
The RDP keyboard layout could only be set per connection, and VDI desktops
never got one, so a Swiss or German user on a desktop set up for their
layout typed with a US keymap: Y and Z swapped and no umlauts.
Settings > My keyboard layout on the Connections page is remembered in that
browser and sent with each connect as keyboard_layout. The server uses it
for an RDP entry left at the server default and for VDI desktops; a layout
set on the entry still wins, and a name guacd has no keymap for is dropped.
The entry editor also lists the nine guacd layouts it was missing.
A desktop wider than the window, such as a two-monitor VNC console viewed
from a laptop, was always scaled to fit and came out too small to read. The
Controls panel now has a View choice: Fit to window (the default) or Actual
size, with scroll bars, which draws it at 100% inside a scrolling container.
The choice is remembered per browser. The scroll bars are styled so they
stay visible where the platform would hide them until you scroll.
A VNC connection can choose how the server encodes its screen for
rustguac: Auto (default, unchanged), LAN (Ultra/ZRLE/Hextile first, low
compression, top JPEG quality), WAN (Tight/ZRLE, compression and JPEG
quality 6) or Slow link (Tight, compression 9, JPEG quality 3, 16-bit
colour unless the connection sets a colour depth). They set guacd's
encodings, compress-level and quality-level; Auto sends none of them.
An unknown preset is refused rather than ignored. Web sessions are
unchanged.
Docs: the VNC section of the overview now covers screen size, the
presets, and which VNC authentication methods work (VNC password,
VeNCrypt, macOS, UltraVNC MS-Logon II) and which don't (RSA-AES,
UltraVNC encryption plugins, MS-Logon III).
guacd asks a VNC server to resize its screen to the browser window by
default. A physical console (UltraVNC, RealVNC, x11vnc) can't, and
libvncclient then stops requesting updates while it waits for a resize
that never comes ("Skipping Update - resize in progress"), so the
session all but freezes. Seen with UltraVNC on a two-monitor console.
VNC entries and ad-hoc VNC now send disable-display-resize unless the
new vnc_resize option is on ("Resize the remote desktop to fit the
window" in the editor), and guacd scales the screen instead. Web
sessions, on our own Xvnc, still resize. The VNC argument mapping moves
into vnc_arg_value() so it can be tested.
In the editor's ad-hoc mode a poweruser was offered file transfer even
when [drive] has it off, and VDI CPU, memory and idle limits above the
site defaults, all of which apply_adhoc_policy drops. The auth status
now says whether the drive is enabled (not only configured), and the
editor hides file transfer from ad-hoc users below admin when it is off
and says the VDI limits are capped. Admins see everything as before.
POST /api/addressbook/adhoc/connect fell through to the change log's
catch-all, so every ad-hoc connect was logged as the whole address book
changing and every open Connections tab reloaded its folder list and
search index. Treat it like connecting to an entry: no change.
guacd hands a PEM CA to spice-gtk, which quietly falls back to the
system trust store if the PEM holds no certificate, so a corrupt CA in
an entry (or from Proxmox) meant trusting every public CA instead of
failing. Each block must now parse as a real certificate, or the session
is refused. Tested with a real certificate and with three corrupt ones.
Ad-hoc sessions had their own form on the Sessions page with a subset
of the options a connection entry has. "Connect to a host" now opens
the Connections entry editor in an ad-hoc mode instead, so an ad-hoc
connection has every option an entry has, now and as entries gain more.
The button sits beside the global search box on every page that has
one. It is shown to poweruser and above under the same rules as the
search box, and opens the editor in place on Connections or goes to
/connections.html#adhoc elsewhere. Ad-hoc mode has no name or folder,
Connect instead of Save, and for admins a "Save as entry" action.
POST /api/addressbook/adhoc/connect takes the entry as the editor
builds it. It resolves only the caller's own credential variables,
never lends folder defaults, and goes through apply_adhoc_policy and
the session rate limit like every other ad-hoc route.
POST /api/sessions is unchanged.
session_request_from_entry() replaces the two copies of the entry to
session request conversion, used by the Connections connect endpoint
and /api/connect, so an option added to entries works in all three.
can_connect_adhoc() is the one place that decides who may connect
ad-hoc.
The Sessions ad-hoc form is removed. loadFolders() now runs its
callback, so #open= links work again.
Refs sol1/rustguac#295
(cherry picked from commit dd40bf08ec)
The screenshots showed v1.9.5 with a custom title, logo and theme, and
folder names that looked like a real instance, so they no longer matched
what a new install shows.
Replace them with captures of v1.12.1 using the default title, logo and
aurora theme, against fully fictional data (example.com users, example
hosts and documentation address ranges), made with contrib/screenshots.
The existing file names are kept so outside links still work. Three views
are new: a live SSH session, a live Web session and the same page in four
built-in themes. The light/ folder holds the twelve page views in the
light theme. The PNGs are compressed with oxipng.
screenshots.md now describes what each image shows, without the
per-version notes.
Fixes#305
(cherry picked from commit 5aae69a7cf)
The screenshots/ folder had no record of how it was made, so every
refresh meant setting up an instance by hand and inventing data again.
contrib/screenshots/ starts rustguac (any image, sol1/rustguac:1.12.1 by
default) with OpenBao in dev mode, a mock OIDC provider and throwaway SSH
and web targets, all on a private compose network with no host ports. A
Playwright container signs in as an admin pre-provisioned with
`rustguac add-user`, creates users, folders, entries, credential
variables and API tokens through the API, runs real SSH and Web sessions
for the recordings and history, and captures the screenshots/ set in the
aurora and light themes. Nothing is written to the database directly,
and all names, hosts and addresses are reserved example data.
The rustguac container runs with seccomp=unconfined because Chromium's
sandbox needs it for Web sessions (#307); the README says so.
Refs #305
(cherry picked from commit 586cfd3e32)
The Docker instructions started the container under Docker's default
seccomp profile, where every web session fails because Chromium's
sandbox cannot create its namespaces, and nothing said why.
- web-sessions.md: a Chromium sandbox section covering the namespace
sandbox, the SUID helper fallback and the new start-up error, and a
Running under Docker subsection with the shipped profile, a Compose
snippet, the user-namespace requirement on the host, and the
alternatives (seccomp=unconfined, CAP_SYS_ADMIN) with their cost;
a troubleshooting entry for the error
- installation.md, README.md, deployment-guide.md: extract the profile
from the image and pass it with --security-opt in the docker run and
Compose examples
- security.md: the sandbox section said Chromium uses the SUID helper;
it prefers its namespace sandbox and uses the helper only as a
fallback. Point Docker users at the profile rather than unconfined
Fixes#307
(cherry picked from commit 305bb6e195)
Copy contrib/docker/seccomp-chromium.json to
/opt/rustguac/seccomp-chromium.json so users of the published image can
extract the profile that matches their image version, the same way the
docs already extract config.toml.default:
docker run --rm --entrypoint cat <image> \
/opt/rustguac/seccomp-chromium.json > seccomp-chromium.json
The file is only read by the Docker client when the container is
created; nothing inside the container uses it.
Refs #307
(cherry picked from commit f432b2bc07)
When Chromium cannot create its sandbox it aborts at start-up, and the
session fails with Chromium's raw stderr, e.g. "Failed to move to new
namespace ... errno = Operation not permitted" followed by a zygote
FATAL. Nothing in that points at the container's seccomp profile, which
is the usual cause under Docker.
Match the four messages Chromium prints for the ways its sandbox can
fail to start, and put the likely cause and the fix in front of the
stderr in both the log and the API error:
- namespaces refused (Docker's default seccomp profile): use
contrib/docker/seccomp-chromium.json
- the SUID helper blocked by no_new_privs (no-new-privileges)
- the SUID helper present but not root-owned with mode 4755
- no namespaces and no helper ("No usable sandbox!")
Each message links the sandbox section of the web sessions docs. The
classification runs on the full stderr, before it is truncated for the
log. Unrelated start-up failures are reported as before.
Refs #307
(cherry picked from commit 397770fd72)
Docker's default seccomp profile only allows clone() and unshare() with
namespace flags when the container has CAP_SYS_ADMIN. Chromium's
namespace sandbox needs new user, PID and network namespaces, so under
the default profile it falls back to the SUID chrome-sandbox helper.
That fails too, because the container has no CAP_SYS_ADMIN, and every
web session aborts with "Failed to move to new namespace ... errno =
Operation not permitted". Until now the only way round it was to run
the container with seccomp=unconfined.
contrib/docker/seccomp-chromium.json is Docker's default profile
(moby/profiles seccomp/v0.2.3) with two changes:
- the clone rules also accept CLONE_NEWUSER, CLONE_NEWPID and
CLONE_NEWNET; mount, UTS, IPC and cgroup namespaces stay refused
- a matching rule allows unshare() for the same three namespace types
With either change alone Chromium still fails to start. Requiring
CLONE_NEWUSER on every such call is too strict, because the zygote
later forks nested PID namespaces from inside its user namespace. Every
other rule is unchanged, and the profile grants no capability.
Refs #307
(cherry picked from commit fbd249dd55)
Add the page to the Documentation lists in the README and overview, and
link it where SPICE and Proxmox come up: the session types in the
overview, the session and entry fields in the API reference, the
vnc_allowed_networks row in the configuration reference, and the entry
types and jump-host table in integrations. Those two lists also now name
SPICE and Proxmox VE, which they had left out, and no longer claim that
every session type can use a jump host, since VDI cannot.
(cherry picked from commit 638c579402)
SPICE and Proxmox VE are headline session types, but what the docs said
about them was scattered across the API field tables and the allowlist
notes. Nothing covered the entry editor, the Proxmox API token, node
auto-detection, the one-time ticket and SPICE proxy flow, TLS options,
whether a given package ships guacd's SPICE plugin, or the common
failures from #195 and #218.
docs/spice-and-proxmox.md covers each of these, checked against the
session, PVE client, guacd and editor code and the build scripts.
Fixes#302
(cherry picked from commit ddd3c23178)
The SPICE CA certificate field and the Proxmox spiceproxy response
both carry PEM text, which rustguac sends as guacd's ca-cert arg. The
SPICE plugin passed that value to spice-gtk's ca-file property, which
is a path. OpenSSL could not open a file named after the certificate,
spice-gtk logged "loading ca certs from -----BEGIN CERTIFICATE-----
... failed", fell back to the system trust store, and rejected any
server signed by a private CA, so verification only worked with
"ignore certificate" turned on.
Add guacd patch 011: when ca-cert contains a PEM header, hand it to
spice-gtk's in-memory ca property (a GByteArray of PEM data, available
since spice-gtk 0.15), which adds each certificate to the TLS trust
store. The certificate never touches disk. Any other value is still
passed as ca-file, so a path keeps working. The settings field is
renamed ca_file to ca_cert to match what it holds.
Fixes#308
(cherry picked from commit 6bfc5b38f2)
The connection editor labels a blank SPICE TLS port "(same as Port)",
and the jump-host path already tunnels tls_port.unwrap_or(port). The
direct path disagreed: with TLS on it sent an empty plain port (so
spice-gtk does not try plaintext) and an empty tls-port when none was
set, and guacd's SPICE plugin then defaulted the TLS port to 5900. A
TLS-only server on any other port was unreachable unless TLS port was
filled in by hand.
Add SpiceParams::dial_port(), which returns the TLS port with TLS on,
falling back to the entry's port, and use it for both the tls-port
connect arg and the jump-host tunnel target so the two paths agree.
The SPICE arg mapping moves into spice_arg_value() so it can be unit
tested, and a stale comment claiming the password is delivered over
argv is dropped.
Proxmox consoles always set tls_port from the spiceproxy response and
are unaffected.
Fixes#309
(cherry picked from commit f5c893ab25)
A few examples used real-world names instead of reserved ones:
- the web session example pointed at www.saucedemo.com, a real
third-party site, with that site's demo login; it now uses
app.example.com and neutral credentials;
- the jump host examples used *.corp.local, and .local is the mDNS
domain, so copied settings resolve oddly; they now use
*.example.internal, as does the Squid proxy example;
- the move-entry response showed the folder "mrgelato"; now
"customer-a";
- the CLI audit log sample showed the user "dave"; now "alice", with
the columns kept aligned;
- the commented-out Knocknoc redirect in 403.http suggested
knocknoc.mycompany.com, a registered domain; now knocknoc.example.com.
Fixes#304
(cherry picked from commit b43de8678a)
installation.md: the Debian post-install steps ran 1-4, 6, 7. No step
was lost: when the Vault step was added as step 4 in 77c7c53, the drive
step after it was renumbered to 6 instead of 5. Number them 5 and 6.
migration.md: "Splitting to multiple Vaults (disaster recovery)" was a
second h1, so the page read as two documents and its Step 1-3 headings
sat at the same level as the Guacamole import's. Make it an h2 with its
steps at h3 and Options at h4. Heading text is unchanged, so every
anchor on the page stays the same. Point the cross-links between this
section and configuration.md's "Multiple Vault backends" at each
other's sections rather than the top of the page.
Fixes#303
(cherry picked from commit 5eebf39839)
The supported session types table for jump hosts listed only SSH, RDP,
VNC and web, although session creation also tunnels SPICE and Proxmox
VE. Add rows for them as the code behaves: SPICE forwards to the TLS
port when TLS is on, and Proxmox VE tunnels the PVE API and the SPICE
proxy separately, with certificate checks as they apply to each hop.
VDI is marked not applicable, since the container is local and the UI
hides the jump host section for it.
Also note how the network allowlists apply through a chain: the target
is resolved by the bastion, and only the first jump host is checked
locally, for ad-hoc sessions from non-admins.
Fixes#301
(cherry picked from commit 15390667b8)
The Docker section of the deployment guide pulled and ran
ghcr.io/sol1/rustguac:latest, but release.yml only pushes to Docker
Hub (sol1/rustguac:<version> and :latest), so the first command failed.
Use sol1/rustguac:latest, as the README and installation.md already do.
Fixes#300
(cherry picked from commit 1a034fc77a)
The entrypoint waited for both processes, so if guacd died the container
kept running with every session failing, and a restart policy never
fired (and the same if rustguac died). Wait for whichever exits first
with bash's wait -n, stop the other, and exit with its status. Tested:
docker stop in 0.3s; killing guacd or rustguac now stops the container.
(cherry picked from commit 30f384e6efa92539c30e80263aa1afd1a21ae370)
When rustguac replaces the PID 1 the shell will never invoke the trap
on exit. This leaves the shell as PID 1 for the trap to be invoked
on exit.
(cherry picked from commit 1e31d74312)
clear_entry_only_fields() missed several fields an ad-hoc caller below
admin could still set, and GET /api/connect's ad-hoc mode applied none
of the checks. apply_adhoc_policy() now runs on both ad-hoc routes. For
callers below admin it fences every target, drops the fields only a
saved entry may set (now also the VDI account, the Kerberos cache path,
sharing and Wake-on-LAN), lets recording be turned on but never off when
it is enabled site-wide, stops the drive being turned on when [drive]
leaves it off, and caps VDI CPU, memory and idle timeout at the [vdi]
defaults. Admins and saved entries are unchanged. Unit tests cover each
rule.
On top of the workload JWT login from #285:
- token_command: a program and its arguments (run directly, never
through a shell) that prints the JWT, run again at every login, with a
timeout. For issuers that hand out a token on request, such as a cloud
metadata service. Its output never reaches an error or the log.
- import-guacamole and vault-migrate log in with jwt_auth too; they
insisted on VAULT_SECRET_ID. One helper, VaultConfig::login_secret,
now decides the secret for the server and both commands.
- jwt_auth is checked before any login: the mount goes into the login
URL, so plain path segments only, and exactly one of path and
token_command.
- Docs: the jwt_auth keys, and setting it up with Nomad workload
identity, SPIRE (spiffe-helper), and cloud VM identity tokens, with a
note on when JWT login really removes the stored secret.
A .guac recording only plays in rustguac's own player. The Recordings
page now has an mp4 button per recording: it starts a conversion,
shows progress, and downloads the video when ready. The file is a
standard MP4 that plays in VLC and browsers.
- POST /api/recordings/:name/mp4 starts it (202 running, 200 ready,
409 while the session is live, 503 without guacenc),
GET .../mp4/status reports it, GET .../mp4 downloads it. Same access
and name checks as the recording download (poweruser or admin).
- guacenc writes <input>.m4v beside its input, so each job links the
recording into a work directory inside the cache and moves the
finished file to <cache>/<stem>.mp4; the recording directory is never
written. guacenc exits 0 even on failure, so success is the output
existing and not being empty. Runs with a timeout, killed on timeout.
- At most mp4_max_jobs at once (default 1). The video is the
recording's own size (its largest layer 0 size), scaled down to fit
1920x1080 and rounded to even.
- Cached MP4s go after mp4_cache_hours (24), when their recording goes,
and all of them before rotation removes any recording for disk space.
Leftover work directories are cleared at startup and hourly.
- The listing carries each recording's mp4 state only when conversion
is available, so the button is hidden on a build without guacenc.
Config: mp4_enabled, guacenc_path, mp4_cache_path, mp4_cache_hours,
mp4_max_jobs, mp4_timeout_secs, mp4_bitrate, mp4_max_width/height under
[recording]. Docs in configuration.md (MP4 downloads, including
converting by hand) and api.md, which also had the recordings role as
operator; it is poweruser.
Tests: the converter (cache layout, failure leaves nothing, live and
missing refused, purge, size parsing and fitting) with a stand-in
guacenc, the shared access check, and a browser spec for the button.
guacenc, from guacamole-server, converts .guac recordings to video and is
what MP4 downloads (#241) run. Stop passing --disable-guacenc in
build-deb.sh, the release workflow, install.sh, the Dockerfile and
dev.sh, and ship /opt/rustguac/bin/guacenc in the .deb (debian/rules),
the Docker runtime image and the release tarball (install-release.sh
installs it when present). It needs only the ffmpeg libraries guacd is
already built and shipped with, so no new dependencies.
configure leaves guacenc out quietly when an ffmpeg library is missing,
like the protocols, so check-guacd-plugins.sh now also fails the build
when guacenc was not built.
POST /api/sessions read address_book_entry, address_book_folder,
entry_display_name, max_recordings and record_typescript from the
request body. A poweruser could name a real entry with a rotation limit
of 1, and when the recorded session ended per-entry rotation deleted
that entry's older recordings; the same fields put a false entry name
on the session in reports and recordings, and turned on a plaintext
typescript the entry's admin had not chosen. For anyone below admin
these fields are now cleared, as the network fence already is.
The proxy raced its two pump tasks against the cancel token with select!,
which consumed the JoinHandles. Dropping a handle only detaches its task,
so when terminate (or disabling a user) fired the token, or when one
direction ended, the other task kept forwarding: traffic still reached
the target and the guacd connection stayed open. Race through a helper
that aborts both tasks afterwards, whichever way it ended. Tested with
tasks that never finish on their own, for cancellation and for one side
ending.
The username is a directory under home_base that is bind-mounted into
the container. An override could carry ../ and the guard compared paths
as text, so it passed. container_username must now be 1-32 letters,
digits, '.', '_' or '-', not starting with '.' or '-'; the Docker driver
also requires exactly one path component and compares real paths, so a
symlink in home_base cannot redirect the mount either.
Users are found by email, and with auto_create_users = false the email
becomes the gate. An email alone is not proof of identity, since some
providers let people set it, so a user is now bound to the OIDC subject
of their first login and a login with the same email but a different
subject is refused (and logged). A login the provider explicitly marks
email_verified: false is refused too.
New CLI-only command reset-oidc-subject releases the binding, for moving
to a different identity provider or a recreated provider account; it is
written to the CLI audit log.
Emails match ignoring case, so an admin typing JSmith@Example.com no
longer locks out a provider that sends jsmith@example.com, nor creates a
second account with auto-create on; a user cannot be added twice in
different case. add-user and POST /api/users check the email and name,
and create_user reports "already exists" as a type instead of matching
"UNIQUE" in the error text.
Tests for the login rules, and CLI integration tests for add-user and
reset-oidc-subject including their audit rows. Docs: how a login is
matched, moving providers, POST /api/users, the CLI commands, and the
login page message (a redirect, not a 403).
With `[oidc] auto_create_users = false`, OIDC login no longer creates
an account for a user who isn't in the database yet. Only users an
admin has added beforehand can log in. The option defaults to true, so
existing deployments behave as before.
An admin can add users ahead of their first login with any of:
- CLI: `rustguac add-user --email <email> --role <role> [--name <name>]`
- API: `POST /api/users` (admin only), returns 201, or 409 if the user exists
- Admin page: an "Add User" form under the users table
A user added this way keeps the role the admin gave them; default_role
is not applied. Group-to-role mappings still apply on every login.
A rejected SSO login now redirects to the login page with an error code
instead of returning JSON. This covers a user with no account
(sso_error=no_account) and a disabled account (sso_error=disabled).
SSO errors are now shown under the SSO button: they used to go into the
API key form, which is hidden when SSO is on, so "SSO login failed" was
never visible. The "provider unavailable" notice uses the same element,
and all SSO messages are defined in one place in index.html.
(cherry picked from commit 9bfdc52155)
The release workflow built guacd for the .deb without --with-spice and
without libspice-client-glib-2.0-dev, so every .deb from v1.9.0 to
v1.11.0 lacked SPICE and Proxmox consoles failed with "Support for
protocol spice is not installed". Docker and install.sh were fine.
guacd's configure leaves out a protocol whose library is missing even
when asked for it, which is why nothing failed. scripts/check-guacd-plugins.sh
now fails the build unless rdp, ssh, vnc and spice were all built; it
runs after make install in release.yml, build-deb.sh, install.sh and the
Dockerfile.
Also:
- --disable-kubernetes, not --without-kubernetes, which configure ignored,
so every build shipped an unused Kubernetes plugin
- the Dockerfile's trailing || true covered the whole guacd build, so a
failed compile still produced an image; it now covers only the
optional FreeRDP plugin copy it was meant for