Files
rustguac/docs/configuration.md
T
Jad Seifeddine d75446f040 connections: open ad-hoc connections in the entry editor
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)
2026-10-20 08:54:46 +11:00

23 KiB

Configuration Reference

rustguac reads a TOML configuration file. All settings have sensible defaults and are optional.

rustguac --config /opt/rustguac/config.toml serve

See config.example.toml for a fully commented reference.

Server settings

Key Default Description
listen_addr 127.0.0.1:8089 Address and port to listen on
guacd_addr 127.0.0.1:4822 guacd TCP address
recording_path ./recordings Session recording directory
static_path ./static Static web files directory
db_path ./rustguac.db SQLite database path
site_title rustguac Browser tab and page header title

Session timeouts

Key Default Description
session_pending_timeout_secs 60 Seconds before pending sessions expire
session_max_duration_secs 28800 (8h) Maximum active session duration
auth_session_ttl_secs 86400 (24h) OIDC auth session cookie TTL

Browser session settings

Key Default Description
xvnc_path Xvnc Path to Xvnc binary
chromium_path chromium Path to Chromium binary
display_range_start 100 First X display number
display_range_end 199 Last X display number
cdp_port_range_start 9200 First Chrome DevTools Protocol port (for login scripts)
cdp_port_range_end 9299 Last CDP port
login_scripts_dir /opt/rustguac/scripts Directory containing login scripts
login_script_timeout_secs 120 Maximum runtime for login scripts before they are killed

Connection allowlists

CIDR ranges controlling which hosts sessions can connect to. All default to localhost only.

Important: These are top-level TOML keys and must appear before any [section] header. Keys placed after a section header (e.g., [tls]) are scoped to that section and will be silently ignored.

Key Default Description
ssh_allowed_networks ["127.0.0.0/8", "::1/128"] Allowed SSH targets (plus first jump hosts and host-key probes for non-admin ad-hoc use)
rdp_allowed_networks ["127.0.0.0/8", "::1/128"] Allowed RDP targets
vnc_allowed_networks ["127.0.0.0/8", "::1/128"] Allowed VNC and SPICE targets (plus Proxmox VE API and SPICE proxy hosts for non-admin ad-hoc use; see SPICE and Proxmox VE)
web_allowed_networks ["127.0.0.0/8", "::1/128"] Allowed web session URL hosts

Trusted proxies

Key Default Description
trusted_proxies [] CIDRs of reverse proxies whose X-Forwarded-For to trust (read right to left, skipping trusted hops)
rate_limit false Enable API rate limiting. Not needed when behind a rate-limiting reverse proxy.
session_history_retention_days 90 Days to keep session history in the database. 0 = keep forever.

[tls] section

Configures TLS for the web server and/or the guacd connection. There is no enabled toggle — the presence of the relevant fields controls behaviour:

  • Server HTTPS: Provide both cert_path and key_path to serve HTTPS. Omit them to serve plain HTTP (useful behind a TLS-terminating reverse proxy like Traefik/HAProxy).
  • guacd TLS: Provide guacd_cert_path to connect to guacd over TLS. This is independent of server HTTPS.

All fields are optional. The [tls] section can contain any combination.

Key Description
cert_path HTTPS certificate path (PEM). Both cert_path and key_path must be set for HTTPS.
key_path HTTPS private key path (PEM). Both cert_path and key_path must be set for HTTPS.
guacd_cert_path Trust certificate for guacd TLS connection (independent of server HTTPS)

Examples:

HTTPS + guacd TLS (self-hosted):

[tls]
cert_path = "/opt/rustguac/tls/cert.pem"
key_path = "/opt/rustguac/tls/key.pem"
guacd_cert_path = "/opt/rustguac/tls/cert.pem"

HTTP server + guacd TLS (behind a reverse proxy):

[tls]
guacd_cert_path = "/opt/rustguac/tls/guacd-cert.pem"

HTTPS only (guacd on localhost, no TLS needed):

[tls]
cert_path = "/opt/rustguac/tls/cert.pem"
key_path = "/opt/rustguac/tls/key.pem"

[oidc] section

Enables OpenID Connect authentication. When configured, the web UI shows a login button. API key auth continues to work alongside OIDC.

Key Default Description
issuer_url — OIDC provider issuer URL (required)
client_id — OIDC client ID (required)
client_secret — OIDC client secret (or use OIDC_CLIENT_SECRET env var)
redirect_uri — Redirect URI: https://your-host/auth/callback (required)
default_role operator Role assigned to new users on first login
auto_create_users true Create an account on first OIDC login. When false, only users already in the database can log in; add them with rustguac add-user, POST /api/users or the Admin page. See Only users you have added
groups_claim groups JWT claim name containing group memberships
extra_scopes [] Additional OIDC scopes to request
ca_cert — Path to CA certificate (PEM) for verifying the OIDC provider
tls_skip_verify false Skip TLS verification (debugging only — exposes secrets to MITM)

Note: issuer_url must match the discovered issuer URI exactly, including default ports and trailing slashes. For example, https://idp.example.com/ and https://idp.example.com may be treated as different issuers. Check your provider's .well-known/openid-configuration for the canonical value.

[vault] section

Enables the Vault-backed connections. Uses AppRole by default, requiring the VAULT_SECRET_ID environment variable. Configure [vault.jwt_auth] to use a workload JWT instead; JWT mode does not require an AppRole role ID or SecretID.

Key Default Description
addr — Vault server address (required)
role_id — AppRole role ID (required unless jwt_auth is configured)
mount secret KV v2 mount path
base_path rustguac Base path under the mount
namespace — Vault Enterprise / OpenBao namespace
instance_name — Instance name for instance-scoped entries
tls_skip_verify false Skip TLS certificate verification (dev only)
ca_cert — Path to custom CA certificate (PEM) for verifying the Vault server
client_cert — Path to client certificate (PEM) for mTLS
client_key — Path to client private key (PEM) for mTLS (required if client_cert is set)

Workload JWT authentication

[vault]
addr = "https://vault.example.com:8200"
mount = "secret"
base_path = "rustguac"

[vault.jwt_auth]
mount = "workload-jwt"
role = "rustguac"
path = "/var/run/secrets/vault/token"
Key Default Description
mount (required) The JWT auth method's mount, not the KV mount, e.g. jwt or auth-k8s/jwt
role (required) The role on that mount
path A file holding the JWT
token_command A program and its arguments that print the JWT, and nothing else, on stdout. Run directly, never through a shell; its output is never logged
token_command_timeout_secs 10 How long token_command may run before it is killed

Give exactly one of path and token_command. The token is fetched again on every login and reauthentication, so a rotated token is always the one used. JWT mode takes precedence over AppRole and never falls back to it when the token is missing, empty or rejected. The import-guacamole and vault-migrate commands log in the same way. Use this with Vault/OpenBao JWT auth configured to verify the token issuer, audience and exact workload identity. This is separate from users' OIDC login.

The same nested block works as [vault_shared.jwt_auth] and [vault_local.jwt_auth]; only AppRole-mode backends need their SecretID env var. See Integrations for Kubernetes, Nomad, SPIRE and cloud VM setups.

Multiple Vault backends (disaster recovery)

By default the single [vault] serves both the shared and instance (local) address-book scopes. For DR across a fleet you can give each scope its own Vault so that one being unreachable cannot take the other down with it. Add either or both of the optional blocks below; each takes the same keys as [vault].

Block Serves Secret ID env var
[vault] Default/fallback for any scope without a dedicated backend; also the home of the LUKS key VAULT_SECRET_ID
[vault_shared] The shared scope VAULT_SHARED_SECRET_ID
[vault_local] The instance (local) scope VAULT_LOCAL_SECRET_ID

A bare [vault] with no overrides behaves exactly as a single-Vault deployment, so nothing changes for existing installs. Each backend connects, retries, and renews its token independently. If a dedicated backend is unreachable, that scope is shown as temporarily unavailable in the Connections tree while the other scopes keep working.

When [vault_local] is used, set its instance_name to the value the data was originally stored under, so the instance/<name>/ paths line up. Splitting an existing single-Vault deployment is a one-time copy with the vault-migrate subcommand (see Splitting to multiple Vaults).

# Primary/local Vault (always reachable on this instance)
[vault]
addr = "https://127.0.0.1:8200"
role_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
instance_name = "dc1"

# Optional central Vault shared across the fleet
[vault_shared]
addr = "https://vault-central.example.com:8200"
role_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

Related top-level setting:

Key Default Description
user_credentials_default_scope local Where a new per-user credential variable is stored when more than one backend is configured: local (stays on this instance, survives a central outage) or shared (propagates fleet-wide). Ignored with a single Vault.

[drive] section

Enables file transfer for RDP (drive redirection) and SSH (SFTP).

Key Default Description
enabled false Enable drive/file transfer
drive_path ./drives Base directory for per-session storage
drive_name Shared Drive Name shown in remote RDP session
allow_download true Allow file download from remote
allow_upload true Allow file upload to remote
cleanup_on_close true Delete session drive directory on disconnect
retention_secs 0 Delay before cleanup (0 = immediate)
luks_device — LUKS container file path
luks_name rustguac-drives Device-mapper name
luks_key_path — Vault KV path for LUKS encryption key

[theme] section

Customises the UI appearance — base preset, individual colours, and logo. All fields are optional. A minimal example:

[theme]
preset = "light"
logo_url = "/acme-logo.png"
primary_color = "#003366"
accent_color = "#FF6600"

See themes.md for the full reference: built-in preset list, every overridable field, the per-user picker, and how to author your own themes as .toml files under <static_path>/themes/ (no recompile needed).

Place the logo file in the static_path directory (e.g. /opt/rustguac/static/acme-logo.png). In Docker, mount it as a volume:

-v /path/to/acme-logo.png:/opt/rustguac/static/acme-logo.png:ro

[recording] section

Controls session recording behaviour and disk management.

Key Type Default Description
path string recording_path Path for recording files. Overrides the top-level recording_path.
enabled bool true Whether recording is enabled globally.
max_disk_percent integer 80 Delete oldest recordings when disk usage exceeds this percent. 0 = disabled.
max_recordings integer 0 Keep at most this many recordings globally. 0 = unlimited.
rotation_interval_secs integer 300 How often (seconds) to run the rotation check.
typescript_path string (unset) Directory for SSH typescript (raw terminal text) files. Unset = disabled. See below.
typescript_name string {connection}-{user}-{date}-{time} Filename template for typescripts. Tokens listed below.
create_typescript_path bool false Ask guacd to create typescript_path if it does not exist.
mp4_enabled bool true Offer recordings as MP4 downloads (see below). Off by itself when guacenc is not installed.
guacenc_path string /opt/rustguac/bin/guacenc The guacenc binary that converts recordings.
mp4_cache_path string <path>/mp4-cache Where converted MP4s are kept.
mp4_cache_hours integer 24 Remove a converted MP4 this many hours after it was made.
mp4_max_jobs integer 1 Conversions that run at once; more wait their turn.
mp4_timeout_secs integer 1800 Give up on a conversion after this many seconds.
mp4_bitrate integer 2000000 Video bitrate, in bits per second.
mp4_max_width, mp4_max_height integer 1920, 1080 Largest video size. Bigger recordings are scaled down to fit, keeping their shape.
[recording]
enabled = true
max_disk_percent = 80
max_recordings = 1000
rotation_interval_secs = 300

SSH typescript recording

The graphical recording above captures the session as a replayable Guacamole stream. For SSH sessions you can additionally write a typescript: a plain-text log of the full terminal output, compatible with the standard script / scriptreplay tools and trivially greppable. This is aimed at audit and compliance (a human-readable record of what was typed and seen on a switch or server).

Typescript recording is per-connection opt-in and off by default. Two things are required: typescript_path must be set here (the global "where"), and the individual connection entry must have Enable typescript recording for this session ticked in its Recording Settings (Connections page, SSH entries only). A connection with the box unticked, or any session that is not SSH, writes no typescript. Ad-hoc SSH connections (Connect to a host) have no entry; for a non-admin they never record a typescript.

The typescript is produced by guacd, so typescript_path must be writable by the guacd process. On a standard install guacd runs as the same rustguac user as the main service, so a path it owns just works; create_typescript_path = true lets guacd create the directory. guacd writes two files per session, NAME and NAME.timing.

When typescripts exist, the Recordings page shows a SSH Typescripts section (poweruser+) listing each one with its name, size, and time. This is list-only by design: the text content is never downloadable through the web UI (a typescript can contain passwords typed at prompts or secrets printed to screen), so a poweruser can confirm a session was recorded while retrieving the actual log still requires direct access to the rustguac host or storage.

[recording]
typescript_path = "/opt/rustguac/data/typescripts"
typescript_name = "{connection}-{user}-{date}-{time}"
create_typescript_path = true

Filename tokens. guacd does not template typescript names itself (it uses the name verbatim and only appends a numeric suffix to avoid overwriting an existing file). rustguac therefore expands its own tokens in typescript_name before handing it over, so each file is identifiable:

Token Expands to
{user} Session username
{connection} Address-book entry name (falls back to the hostname for ad-hoc sessions)
{host} Target hostname
{date} Connect date, UTC YYYYMMDD
{time} Connect time, UTC HHMMSS
{session} First 8 characters of the session id

Substituted values are sanitised to [A-Za-z0-9_-] (everything else becomes -), so usernames like alice@example.com and free-text entry names are always reduced to a safe basename with no path separators. Unknown {tokens} are left untouched.

Note: these are rustguac's own tokens, not guacd's, and they are unrelated to credential variables (which use $name syntax and apply only to connection-entry credential fields). guacd's own ${GUAC_*} tokens are not interpreted for typescripts.

Keystroke logging in the graphical recording (guacd's recording-include-keys, parseable by guaclog) is a separate mechanism that depends on guacd-driven graphical recording, which rustguac does not use (it records the proxied stream itself). It is therefore not wired up; the typescript is the supported text-audit path.

MP4 downloads

A .guac recording only plays in rustguac's own player. The Recordings page can also give you an MP4 that plays anywhere (VLC, a browser, a video editor): click mp4 next to a recording. rustguac converts it in the background with guacenc, which comes with guacamole-server, and the file downloads when it is ready. Converting a long session takes a while, so the button shows converting... until then.

  • The same people can get the MP4 as can download the recording (poweruser and admin).
  • A recording whose session is still running is not converted, since the file is not finished.
  • Converted files are kept in mp4_cache_path for mp4_cache_hours, so a second download is instant, and removed when their recording is. When rotation needs disk space it removes them all before touching any recording.
  • The video is the recording's own size, scaled down to fit mp4_max_width x mp4_max_height.
  • The button only appears when guacenc is installed. It is part of the .deb, the Docker image and install.sh from 1.11.1; on a build without it the button is not shown.

To convert recordings yourself, for example a batch for an audit, run guacenc on the rustguac host. It writes <recording>.m4v (an MP4 file) next to the input, so copy recordings somewhere writable first:

cp /opt/rustguac/recordings/<id>.guac /tmp/
/opt/rustguac/bin/guacenc -s 1920x1080 -r 2000000 /tmp/<id>.guac
# -> /tmp/<id>.guac.m4v

guacenc reports failures in its output but exits 0 either way, so check that the .m4v file exists.

Encryption at rest (LUKS)

Typescripts are written in plain text. For encryption at rest with no extra infrastructure, point typescript_path at a subdirectory of the LUKS-encrypted drive volume rustguac already manages (see the [drive] section). rustguac opens and mounts that volume at startup (key fetched from Vault) and unmounts it at shutdown, so the directory is available whenever rustguac is running, and the files are encrypted on the block device at rest (powered off, disk theft, block-device backups).

[drive]
drive_path = "/mnt/rustguac-drives"
luks_device = "/dev/disk/by-uuid/..."   # your LUKS volume
luks_key_path = "secret/rustguac/luks"  # Vault KV path holding the key

[recording]
typescript_path = "/mnt/rustguac-drives/typescripts"
create_typescript_path = true

This is the recommended way to protect typescripts at rest today. Note its limits: while rustguac is running the volume is mounted, so the files are plain text to anyone with host access (the same threat model as the drive feature), and it is one key for the whole volume rather than per-connection. Per-file, per-connection-key encryption is tracked as a separate feature request, pending demand.

[vdi] section

Enables VDI (Virtual Desktop Infrastructure) sessions using Docker containers. Each user gets an ephemeral Linux desktop in a Docker container, accessed via xrdp through guacd.

Prerequisites: Docker must be installed on the host and the rustguac user must be in the docker group. See VDI Desktop Containers for full setup.

Key Type Default Description
enabled bool false Enable VDI sessions.
docker_socket string /var/run/docker.sock Docker daemon socket path.
default_cpu_limit float 0 Default CPU limit for containers (fractional cores, e.g. 2.0). 0 = no limit.
default_memory_limit integer 0 Default memory limit in MB. 0 = no limit.
ready_timeout_secs integer 30 Seconds to wait for xrdp to become ready in a new container.
port_range_start integer (none) First localhost port Docker may bind VDI RDP to. Must be set with port_range_end.
port_range_end integer (none) Last localhost port Docker may bind VDI RDP to. Must be set with port_range_start.
container_hook_script string (none) Optional VDI container hook script. Called as <script> up <port> <container_id> <container_name> before readiness checks and <script> down <port> <container_id> <container_name> before removal.
container_hook_timeout_secs integer 10 Seconds to wait for the VDI container hook script.
idle_timeout_mins integer 60 Minutes a container persists after last session disconnect. 0 = remove immediately.
allowed_images list [] Allowed Docker images (exact match). Empty = allow all.
home_base string (none) Base directory for persistent user home dirs. Each user gets {home_base}/{username} mounted into the container.
[vdi]
enabled = true
idle_timeout_mins = 60
# port_range_start = 39000
# port_range_end = 39999
# container_hook_script = "/opt/rustguac/vdi-container-hook.sh"
# container_hook_timeout_secs = 10
home_base = "/vdi-homes"
# allowed_images = ["myregistry/desktop:latest"]

Environment variables

Variable Description
OIDC_CLIENT_SECRET Override OIDC client secret from config file
VAULT_SECRET_ID Vault AppRole secret ID for [vault]
VAULT_SHARED_SECRET_ID Vault AppRole secret ID for [vault_shared] (only if configured)
VAULT_LOCAL_SECRET_ID Vault AppRole secret ID for [vault_local] (only if configured)
RUST_LOG Log level (e.g., info, debug, rustguac=debug)

Setting environment variables for systemd

The shipped systemd unit (rustguac.service) does not include an EnvironmentFile directive by default. To provide secrets like VAULT_SECRET_ID and OIDC_CLIENT_SECRET, create a systemd drop-in override:

1. Create the env file with your secrets:

cat > /opt/rustguac/env <<'EOF'
VAULT_SECRET_ID=your-vault-secret-id
OIDC_CLIENT_SECRET=your-oidc-client-secret
EOF
chmod 600 /opt/rustguac/env
chown rustguac:rustguac /opt/rustguac/env

2. Create a systemd override to load the env file:

sudo systemctl edit rustguac

This opens an editor. Add the following:

[Service]
EnvironmentFile=/opt/rustguac/env

Save and close. This creates a drop-in file at /etc/systemd/system/rustguac.service.d/override.conf.

3. Reload and restart:

sudo systemctl daemon-reload
sudo systemctl restart rustguac

The override persists across package upgrades — dpkg will not overwrite files in the .d/ directory.

Verifying the environment

To confirm the env file is loaded:

sudo systemctl show rustguac | grep EnvironmentFile

You should see:

EnvironmentFile=/opt/rustguac/env