Themes were a Vec hardcoded in src/config.rs (builtin_presets()) - every new preset required editing Rust, recompiling, and shipping a new release, for what is purely presentation data. PR #148 from @dav0l surfaced this nicely by failing to compile on a brace count in the array. This change adds a config::load_themes(&static_path) loader that starts from the eight built-in presets (unchanged) and then merges in any *.toml files from <static_path>/themes/. Disk themes can add new entries or override a built-in by using the same name; the existing builtin_presets() remains as the always-available fallback when the themes directory is missing or empty. File format: flat TOML table, one file per theme, filename (minus extension) is the theme id. See static/themes/catppuccin-macchiato.toml for a full example. Theme names are validated against the same allowlist we use for Vault entry names ([a-zA-Z0-9_-]{1,64}) so they are safe to render in the UI picker and in log lines, and can't be used for path traversal or homoglyph mischief via crafted filenames. dav0l's Catppuccin Macchiato palette lands here as static/themes/catppuccin-macchiato.toml - their submission is the first user-contributed theme under the new mechanism. Closes #148. Backward compatibility: explicit. ThemeConfig::resolve() is now a thin wrapper over resolve_with(builtins), so existing test callers and any production callers see no behavioural change. Existing [theme] sections in user config.toml files - preset only, preset + overrides, overrides only, empty section, typo'd preset - all resolve byte-equal to 1.7.0 (verified by the new existing_user_config_with_theme_section_keeps_working_after_upgrade test). Eight new tests in total cover the loader, the override behaviour, the filename validation, and the upgrade scenario. Docs broken out: themes get docs/themes.md (the full reference); docs/configuration.md is trimmed to a brief stub and pointer. No build-system changes needed - debian/rules, install.sh and the Dockerfile all use recursive `cp -r static/` so the new themes subdirectory is picked up automatically.
12 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 |
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 targets |
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 |
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_pathandkey_pathto serve HTTPS. Omit them to serve plain HTTP (useful behind a TLS-terminating reverse proxy like Traefik/HAProxy). - guacd TLS: Provide
guacd_cert_pathto 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 |
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. Requires VAULT_SECRET_ID environment variable.
| Key | Default | Description |
|---|---|---|
addr |
— | Vault server address (required) |
role_id |
— | AppRole role ID (required) |
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) |
[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. |
[recording]
enabled = true
max_disk_percent = 80
max_recordings = 1000
rotation_interval_secs = 300
[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 |
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