A lightweight Rust replacement for the Apache Guacamole Java webapp. Provides browser-based SSH, RDP, and web browsing sessions through guacd (the Guacamole protocol daemon).
rustguac sits between web browsers and guacd, proxying the Guacamole protocol over WebSockets. It manages session lifecycle, authentication (API keys and OIDC SSO), session recording, and browser-based VNC sessions (Xvnc + Chromium).
- SSH sessions — browser-based SSH terminal via guacd, with ephemeral keypair or manual private key auth
- RDP sessions — connect to Windows/RDP hosts via guacd
- Web browser sessions — headless Chromium on Xvnc, streamed to the browser via VNC
- OIDC single sign-on — authenticate users via any OpenID Connect provider (JumpCloud, Google, Okta, etc.)
- Role-based access — admin, operator, and viewer roles for both API key and OIDC users
- TLS everywhere — HTTPS for clients, TLS between rustguac and guacd (both enabled by default)
- Session recording — all sessions recorded in Guacamole format with playback UI
- Session sharing — share tokens for read-only or collaborative access
- API key auth — SHA-256 hashed keys with IP allowlists and expiry
- Pre-session banners — configurable banner text shown before session starts
- SQLite storage — no external database server needed
- Single binary — just rustguac + guacd, no Java stack
Browser (HTML/JS)
|
| WebSocket over HTTPS
v
rustguac (Rust, axum)
|
| TLS (Guacamole protocol)
v
guacd (C, from guacamole-server)
|
+---> SSH server (for SSH sessions)
+---> RDP server (for RDP sessions)
+---> Xvnc display (for web browser sessions)
|
+---> Chromium (kiosk mode)
Both links are encrypted by default: HTTPS between browsers and rustguac, TLS between rustguac and guacd.
| Port | Service |
|---|---|
| 443 | rustguac HTTPS (default with TLS) |
| 8089 | rustguac HTTP (when TLS is disabled) |
| 4822 | guacd (TLS-encrypted, loopback only) |
| 6000-6099 | Xvnc displays (:100-:199, internal) |
sudo ./install.shThis builds guacd from source (with patches for FreeRDP 3.x compatibility applied automatically), builds rustguac, generates a self-signed TLS certificate, and installs everything to /opt/rustguac with systemd services. The rustguac-to-guacd connection is TLS-encrypted by default.
Flags:
--no-tls— skip TLS cert generation, listen on HTTP port 8089--hostname=FQDN— hostname for the TLS certificate (default: system hostname)--deps-only— only install system packages--no-deps— skip apt install
After install:
# Create an admin and get an API key
/opt/rustguac/bin/rustguac --config /opt/rustguac/config.toml add-admin --name admin
# Start the services (starts both guacd and rustguac)
sudo systemctl start rustguacdocker pull sol1/rustguac:latest
docker run -d -p 8089:8089 sol1/rustguac:latestPre-built images are available on Docker Hub. To build from source instead:
docker build -t rustguac .
docker run -d -p 8089:8089 rustguacThe Docker image generates a self-signed cert at build time and enables TLS between rustguac and guacd by default. The external-facing port is HTTP on 8089 (put a reverse proxy in front for HTTPS in production).
# Clone guacamole-server alongside rustguac
git clone https://github.com/apache/guacamole-server.git ../guacamole-server
# Install build deps, build guacd (patches applied automatically), build + run rustguac
./dev.sh deps
./dev.sh build-guacd
./dev.sh startIf a config.local.toml file exists in the project root, dev.sh run and dev.sh start will use it automatically:
# Generate self-signed TLS cert for dev
./dev.sh generate-cert
cat > config.local.toml <<EOF
[tls]
cert_path = "cert.pem"
key_path = "key.pem"
guacd_cert_path = "cert.pem"
EOF
./dev.sh startrustguac reads a TOML config file (default: config.local.toml, or specify with --config).
listen_addr = "0.0.0.0:443"
guacd_addr = "127.0.0.1:4822"
recording_path = "/opt/rustguac/recordings"
static_path = "/opt/rustguac/static"
db_path = "/opt/rustguac/data/rustguac.db"
session_pending_timeout_secs = 60
session_max_duration_secs = 28800 # 8 hours
site_title = "rustguac"
# Browser session settings
xvnc_path = "Xvnc"
chromium_path = "chromium"
display_range_start = 100
display_range_end = 199All settings have defaults and are optional. See config.example.toml for a fully documented reference.
Control which hosts sessions can connect to. Each setting is a list of CIDR ranges. All three default to localhost only (127.0.0.0/8 and ::1/128).
Important: These are top-level TOML keys and must appear before any [section] header (e.g. [tls], [oidc]). In TOML, any key after a [section] header is scoped to that section, so placing these under [tls] will silently ignore them and the defaults (localhost only) will be used.
# Allow SSH sessions to reach any private network host
ssh_allowed_networks = ["127.0.0.0/8", "::1/128", "10.0.0.0/8", "192.168.0.0/16"]
# Allow RDP to a specific subnet
rdp_allowed_networks = ["10.0.5.0/24"]
# Allow web browser sessions to reach any host
web_allowed_networks = ["0.0.0.0/0", "::/0"]For SSH and RDP sessions, the target hostname is resolved and checked against the allowlist. For web sessions, the URL's hostname is resolved and checked. If resolution returns multiple addresses, at least one must be in the allowlist.
The [tls] section controls both HTTPS for clients and TLS encryption to guacd:
[tls]
cert_path = "/opt/rustguac/tls/cert.pem" # HTTPS certificate
key_path = "/opt/rustguac/tls/key.pem" # HTTPS private key
guacd_cert_path = "/opt/rustguac/tls/cert.pem" # trust this cert for guacd connectioncert_path+key_path— enables HTTPS. Omit the entire[tls]section for plain HTTP.guacd_cert_path— when set, rustguac connects to guacd over TLS, trusting this certificate. The same self-signed cert can serve both purposes. Omit for plain TCP to guacd.
guacd must also be started with TLS flags to match:
guacd -b 127.0.0.1 -l 4822 -L info -f -C /opt/rustguac/tls/cert.pem -K /opt/rustguac/tls/key.pemThe install script and Docker image configure this automatically. Both sides must agree: either both TLS or both plain TCP.
Generate a self-signed certificate:
rustguac generate-cert --hostname your-hostname.example.com --out-dir /opt/rustguac/tlsTo enable OpenID Connect single sign-on, add an [oidc] section:
[oidc]
issuer_url = "https://oauth.id.jumpcloud.com/"
client_id = "your-client-id"
client_secret = "your-client-secret"
redirect_uri = "https://your-host/auth/callback"
default_role = "operator" # role assigned to new users (default: "operator")Works with any OIDC provider: JumpCloud, Google, Okta, Azure AD, Keycloak, etc. Configure your provider with the redirect URI https://your-host/auth/callback.
The client_secret can also be provided via the OIDC_CLIENT_SECRET environment variable, which takes precedence over the config file. This is recommended for production (Docker, systemd EnvironmentFile, etc.).
Roles:
| Role | Level | Permissions |
|---|---|---|
| admin | 4 | Full access: manage users, address book, recordings, sessions, group mappings |
| poweruser | 3 | Ad-hoc session creation + address book connect |
| operator | 2 | Address book connect only (no ad-hoc sessions) |
| viewer | 1 | Read-only: view sessions and recordings |
New OIDC users are assigned default_role on first login. Admins can change roles via CLI, API, or the web Admin page.
Group-to-role mappings: Admins can configure automatic role assignment based on OIDC group membership in the Admin page. Mappings are evaluated on every OIDC login — the highest matching role wins. If no mappings match, the existing role is preserved.
Admins can change roles:
# CLI
rustguac set-role --email user@example.com --role admin
# Or via API
curl -X PUT https://host/api/users/user@example.com/role \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'User management commands:
rustguac list-users
rustguac set-role --email user@example.com --role operator
rustguac disable-user --email user@example.com
rustguac delete-user --email user@example.comWhen OIDC is configured, the web UI shows a login button that redirects to the provider. API key auth continues to work alongside OIDC.
The address book stores connection entries (SSH, RDP, Web) in HashiCorp Vault or OpenBao KV v2. Credentials never reach the browser — rustguac reads them from Vault server-side and creates sessions directly.
1. Enable KV v2 (skip if already enabled):
vault secrets enable -path=secret kv-v22. Create a Vault policy for rustguac:
vault policy write rustguac - <<'EOF'
# Allow rustguac to manage address book entries
path "secret/data/rustguac/*" {
capabilities = ["create", "read", "update", "delete"]
}
path "secret/metadata/rustguac/*" {
capabilities = ["list", "read", "delete"]
}
EOFAdjust secret and rustguac if you use a different mount or base_path.
3. Enable AppRole auth and create a role:
vault auth enable approle
vault write auth/approle/role/rustguac \
token_policies="rustguac" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=0
# Get the role_id (put this in config.toml)
vault read auth/approle/role/rustguac/role-id
# Generate a secret_id (set as VAULT_SECRET_ID env var)
vault write -f auth/approle/role/rustguac/secret-id4. Configure rustguac — add a [vault] section:
[vault]
addr = "https://vault.example.com:8200"
role_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# mount = "secret" # KV v2 mount (default: "secret")
# base_path = "rustguac" # base path under mount (default: "rustguac")
# namespace = "my-ns" # optional, Vault Enterprise / OpenBao namespaces
# instance_name = "prod-1" # optional, enables instance-scoped entriesSet the environment variable:
export VAULT_SECRET_ID="<secret_id from step 3>"For systemd, add it to an EnvironmentFile:
echo 'VAULT_SECRET_ID=<secret_id>' > /opt/rustguac/env
chmod 600 /opt/rustguac/env
# In the systemd unit: EnvironmentFile=/opt/rustguac/env5. Manage via the UI. Admins can create folders (with OIDC group access controls) and connection entries in the Address Book page. Operators can connect to entries in folders their groups have access to.
Vault KV v2 path structure:
| Path | Description |
|---|---|
rustguac/shared/<folder>/.config |
Folder metadata: {"allowed_groups":[...], "description":"..."} |
rustguac/shared/<folder>/<entry> |
Connection entry (shared across all instances) |
rustguac/instance/<name>/<folder>/<entry> |
Instance-specific entry (requires instance_name in config) |
API key admins always have full admin-level access.
# Add an admin (prints the API key — save it)
rustguac add-admin --name myadmin
# Restrict to specific IPs
rustguac add-admin --name myadmin --allowed-ips "10.0.0.0/8,192.168.1.0/24"
# Set expiry
rustguac add-admin --name myadmin --expires "2026-12-31T00:00:00Z"
# List / disable / enable / rotate / delete
rustguac list-admins
rustguac disable-admin --name myadmin
rustguac enable-admin --name myadmin
rustguac rotate-key --name myadmin
rustguac delete-admin --name myadminCreate an SSH session (password auth):
curl -X POST https://localhost/api/sessions \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"session_type": "ssh",
"hostname": "10.0.0.1",
"port": 22,
"username": "root",
"password": "secret"
}'Instead of passwords, rustguac can generate a one-time Ed25519 keypair per session. The flow:
- Create the session with
"generate_keypair": true - Open the session URL (or send the share link — recipients also see the banner)
- The banner displays the public key with a "Copy public key" button — copy it and add to the user's
authorized_keyson the target host - Click "Continue" — only then does rustguac connect to guacd and attempt SSH auth with the ephemeral key
- The private key only exists in memory during the handshake. It is never stored on disk or returned by the API.
The SSH connection is intentionally deferred — guacd does not contact the target host until Continue is clicked, giving time to install the key. This works well with share links: create the session, send the share URL to the person who controls the target host, they install the key and click Continue.
curl -X POST https://localhost/api/sessions \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"session_type": "ssh",
"hostname": "10.0.0.1",
"username": "root",
"generate_keypair": true
}'You can also paste an existing OpenSSH private key via the "private_key" field instead of using ephemeral generation.
Create an RDP session:
curl -X POST https://localhost/api/sessions \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"session_type": "rdp",
"hostname": "10.0.0.1",
"port": 3389,
"username": "Administrator",
"password": "secret",
"ignore_cert": true
}'Create a web browser session:
curl -X POST https://localhost/api/sessions \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"session_type": "web",
"url": "https://example.com"
}'The response includes client_url — open it in a browser to use the session.
Other endpoints:
# List sessions
curl https://localhost/api/sessions -H "Authorization: Bearer <api-key>"
# Delete a session
curl -X DELETE https://localhost/api/sessions/<id> -H "Authorization: Bearer <api-key>"
# List recordings
curl https://localhost/api/recordings -H "Authorization: Bearer <api-key>"
# Health check (no auth)
curl https://localhost/api/healthExample for integrating with an existing stack:
services:
rustguac:
image: sol1/rustguac:latest
ports:
- "8089:8089"
volumes:
- rustguac-data:/opt/rustguac/data
environment:
- RUST_LOG=info
volumes:
rustguac-data:docker build -t sol1/rustguac:latest .
docker push sol1/rustguac:latestFor bare-metal installs, rustguac requires:
- Rust toolchain (1.75+)
- guacd (built from guacamole-server source)
- Xvnc (tigervnc-standalone-server) — for web browser sessions
- Chromium — for web browser sessions
- Build libraries for guacd: libcairo2, libjpeg, libpng, libwebp, libssh2, libssl, libvncserver, libpango, libpulse, ffmpeg libs, freerdp3
See install.sh for the full package list.
guacamole-server (guacd) requires patches to build and run correctly with FreeRDP 3.15+ as shipped in Debian 13. These patches are in the patches/ directory and are applied automatically by all build scripts (build-deb.sh, build-rpm.sh, install.sh, dev.sh, and the Dockerfile).
The patches fix:
- Autoconf feature detection failures caused by
-Werror+ deprecated FreeRDP headers - Deprecated function pointer API (
->input->MouseEvent()etc.) replaced with safe FreeRDP 3.x functions - NULL pointer dereference in the display update channel when FreeRDP fires events before initialization
See patches/README.md for full details. To add new patches, edit ../guacamole-server and export with git diff > patches/NNN-description.patch.
src/
main.rs Entry point, CLI, server setup
api.rs REST API endpoints
auth.rs API key + OIDC session authentication middleware
browser.rs Xvnc + Chromium process manager
config.rs TOML config loading
db.rs SQLite database (admins, OIDC users, sessions)
guacd.rs guacd TLS/TCP connection & protocol handshake
oidc.rs OpenID Connect login flow
protocol.rs Guacamole wire format parser
session.rs Session state machine
websocket.rs WebSocket <-> guacd proxy
static/
*.html Web UI pages
guac/ Guacamole JS client library
Apache License 2.0 — see LICENSE for details.