Skip to content

Attack Surface

Dave Kempe edited this page Apr 26, 2026 · 1 revision

Attack surface

This page is the "what does an attacker see, and what do we do about it" map. Useful for security reviews, threat modelling, and answering "have you thought about X" questions in tender responses.

For the catalogue of defensive features (how to enable rate limiting, where the audit log lives, etc.), see the in-repo docs/security.md. This page is the inverse: where the holes are if any of those defences fail.

What's exposed

In a typical deployment:

Surface Exposed to Auth
https://<host>/ (login UI) Public internet (or Knocknoc-gated) None at the HTTP layer; OIDC starts here
/auth/oidc/login, /auth/oidc/callback Public Owned by the OIDC handshake
/api/* Public (cookie or API key) OIDC session cookie or Authorization: Bearer rgu_...
/ws/<session_id> Public OIDC session cookie or one-shot WebSocket ticket
/share/<token> Public Share token (HMAC, time-bounded)
/static/*, /guac/* Public Static assets, no auth
guacd TCP port (4822) Loopback only TLS, no other auth (rustguac is the only thing that should reach it)
Xvnc displays (6000-6099) Loopback only None (one-time per-session display number)
Docker socket Local Unix socket Filesystem permissions (docker group)
Vault HTTPS rustguac to Vault AppRole + token

Things we worry about

Internet-facing login page

An exposed login page is a probe target. Attackers don't even need to break OIDC; they can fingerprint the application, look up known CVEs, or run automated credential-stuffing against any auth that ends up in a redirect chain.

Mitigations in rustguac:

  • OIDC handles authentication; we don't do password forms ourselves, so there's nothing to credential-stuff.
  • tower_governor rate-limits /auth/* per-IP.
  • The groups claim coming back from the IdP is bounded (64 entries x 256 bytes) so a malformed/hostile IdP can't bloat the seen_groups table.
  • WebSocket Origin/Host check (CSWSH) on /ws/* rejects cross-origin upgrade attempts.

Mitigations outside rustguac:

  • Knocknoc integration gates / behind identity + MFA at the HAProxy layer. The login page returns 403 to anyone who hasn't authenticated to Knocknoc first.
  • Reverse proxy WAF rules (HAProxy ACLs, Cloudflare, etc.) for known bad patterns.

Share tokens

Share tokens let third parties join a session without an account. The blast radius is one session. We've audited this surface a few times; the shape that's fallen out:

  • Tokens are HMAC-signed, with the session ID baked in.
  • Each token has a TTL, an issuer (the user who minted it), and a role (read-only viewer or full collaborator).
  • Every shadow-token use is logged in token_audit_log with the connecting IP. Mint events were already logged; v1.6.1 added the use-event so a leaked token is visible every time it's redeemed within its TTL.
  • Per-entry allow_sharing flag (admin-controlled) gates whether the Share button appears at all.

A leaked share token is bad but bounded: the session is one user's, the entry has a known target, the audit log shows where the token was redeemed from. Rotate the entry's password and revoke the session.

API keys

API keys are SHA-256-hashed, IP-allowlisted, expiry-bounded, and role-scoped (viewer / operator / poweruser / admin). Created via rustguac add-token (CLI) or the admin UI.

Watch for:

  • API keys committed to git or CI logs. We redact them in our own logs ({redacted} placeholder for any string starting with rgu_); your CI / external systems are on you.
  • Old keys still in the database. The admin UI shows last-used timestamps; rotate or delete keys that haven't been used in a while.

Vault path traversal

Connection entries live in Vault at <base>/shared/<folder>/<entry> or <base>/instance/<name>/<folder>/<entry>. A path-traversal bug here would let an authenticated user read entries outside their allowed folder.

What's enforced:

  • vault.rs:validate_name() rejects any name containing /, \, control chars, or anything that isn't [a-zA-Z0-9._-]. Names are 1-64 chars.
  • vault.rs:validate_path() does the same per-segment for folder paths.
  • .config, ., .. are reserved.
  • v1.5.4 closed an audit finding here; the validation is unit-tested for the obvious traversal patterns.

Recordings

Recordings are stored as <recording_path>/<session_id>.guac with companion .meta (JSON metadata) and .thumb (JPEG) files. They contain the full session: keystrokes, screen captures, file transfers.

Access controls:

  • Recordings list is poweruser-scoped (added in v1.5.4; viewers don't see them).
  • File mode is 0o640 and ownership matches the rustguac user.
  • Recordings can contain credentials typed into RDP/SSH login screens. Treat them as sensitive.

VDI containers

Per-user Docker containers run with nosuid,nodev mount options. They get a generated random password per session (or chpasswd-updated on container reuse). Container names are deterministic (rustguac-vdi-<username>) so a single user can't spawn arbitrary container counts.

Watch for:

  • BYO VDI images. If you let users pick container_image, you're trusting whatever's in those images. The [vdi].allowed_images config gates this to a list.
  • Docker socket access. The rustguac user is in the docker group, which is essentially root-equivalent on the host. Accept this in your threat model or run rustguac in a separate VM.

Recording disk fill

Long sessions can produce huge recordings. We added rotation in v1.0.x: per-entry max_recordings, global max_disk percent threshold, time-based cleanup interval. Configure in [recording] section. If recordings are filling disk, audit the per-entry caps.

OIDC trust

We trust the OIDC IdP to assert identity and groups. If your IdP is compromised, you're in trouble regardless of rustguac. We do clamp the groups claim to 64 x 256 bytes to protect against an IdP returning unbounded data.

What we explicitly trust

  • guacd. We bundle it from upstream and treat it as a trusted process. Bugs in guacd's RDP/VNC/SSH parsers would be guacd CVEs, not rustguac CVEs.
  • The host OS. rustguac runs as a non-privileged system user but doesn't sandbox itself further. Container or VM isolation is your job.
  • The reverse proxy. rustguac trusts X-Forwarded-For and similar headers from trusted_proxies (configurable). If you don't list them, headers are ignored. Don't add a public IP to that list.
  • Vault. Connection credentials are written to Vault at admin time and read on connect. We don't cache them in the SQLite DB.

Reporting issues

Security issues should go to the project maintainers privately first. The repo's SECURITY.md (if present) has the contact details. We'd rather hear from you under embargo than read it on a CVE feed.

See also

Clone this wiki locally