Skip to main content

Guacamole - Remote Access

Description

Apache Guacamole is a remote access gateway with a web frontend. It allows the user to connect to a device using SSH, VNC, or RDP using just a web browser.

Deployment details

Guacamole requires three separate Docker containers: (1) the backend server which handles the underlying connections, (2) the frontend server, which provides the web interface, and (3) a PostgreSQL database which stores the frontend's data.

These are the respective Docker container images:

  1. kiankasad/guacd:latest
  2. guacamole/guacamole:latest
  3. ghcr.io/kdkasad/guacdb:2022.09.06

Access

Guacamole is published behind the Secure Web Application Gateway at swag.kasad.com/guacamole. It is protected by Cloudflare Zero Trust, requiring authentication to access.

Custom database container

Guacamole will not automatically initialize a database the first time it is run. Instead, this has to be done manually using an initialization script when creating the database container. To make this easier, I've created a Docker image specifically for Guacamole which will automatically extract the latest initialization script and create the database the first time it's run.

The sources are available on GitHub and the image is published to the GitHub Container Registry as ghcr.io/kdkasad/guacdb.

Custom guacd container

The upstream Guacamole project publishes nightly builds of the guacamole/guacd Docker image. However, the latest one (published 2022-09-06) broke support for Ed25519 SSH keys. Until the next release occurs, I've built and published my own guacd container using the latest sources from GitHub (commit 0361adc).

This container uses the latest upstream sources and is published on Docker Hub as kiankasad/guacd:latest.

Docker Compose stack

The Guacamole stack uses the following Docker Compose configuration:

version: '3'

services:

  guacdb:
    image: ghcr.io/kdkasad/guacdb:2022.09.06
    container_name: guacdb
    restart: unless-stopped
    environment:
      POSTGRES_DB: guacamole_db
      POSTGRES_USER: guac
      POSTGRES_PASSWORD: [redacted]
    volumes:
      - ./guacdb-data:/var/lib/postgresql/data

  guacd:
    image: kiankasad/guacd:latest
    container_name: guacd
    restart: unless-stopped

  guacamole:
    image: guacamole/guacamole:latest
    container_name: guacamole
    restart: unless-stopped
    environment:
      REMOTE_IP_VALVE_ENABLED: true
      GUACD_HOSTNAME: guacd
      POSTGRES_HOSTNAME: guacdb
      POSTGRES_DATABASE: guacamole_db
      POSTGRES_USER: guac
      POSTGRES_PASSWORD: [redacted]
      POSTGRESQL_AUTO_CREATE_ACCOUNTS: true
      OPENID_AUTHORIZATION_ENDPOINT: "https://auth2.kasad.com/application/o/authorize/"
      OPENID_JWKS_ENDPOINT: "https://auth2.kasad.com/application/o/guacamole/jwks/"
      OPENID_ISSUER: "https://auth2.kasad.com/"
      OPENID_CLIENT_ID: "################################"
      OPENID_REDIRECT_URI: "https://swag.kasad.com/guacamole/"
      OPENID_USERNAME_CLAIM_TYPE: "preferred_username"
      EXTENSION_PRIORITY: "openid"
    depends_on:
      - guacdb
      - guacd
    networks:
      - default
      - swag

networks:
  default:
    ipam:
      driver: default
      config:
        - subnet: "172.18.0.0/16"
          gateway: "172.18.0.1"
  swag:
    name: swag_default
    external: true

Static network subnet

The reason for the specific network subnet is that one of the connections within Guacamole is to Kian's laptop. Since Kian's laptop is the host of the Docker containers, the easiest way to address it is as 172.18.0.1. This requires that the subnet for the Guacamole stack is always at least 172.18.0.0/24. I've set it to 172.18.0.0/16 because Docker usually assigns 16-bit subnets.

SWAG reverse proxy

The web frontend for Guacamole is reverse-proxied behind the Secure Web Application Gateway (SWAG). This means the swag container needs network access to the guacamole container, so the guacamole container is added to the swag_default network in the Compose stack.

Configuration

The Guacamole frontend is where the majority of the configuration happens, as it also handles authentication and storage of user preferences/data.

Guacamole is configured using a guacamole.properties file. However, the Docker container allows for automatic generation of this configuration file using environment variables. So all of the configuration for Guacamole is done using environment variables in the Docker Compose file.

Single Sign-On

Guacamole's frontend server utilizes extensions to provide authentication backends. We use the openid and postgresql authentication extensions. OpenID Connect interfaces with Authentik to provide user authentication. The PostgreSQL backend stores the Guacamole-specific data for each user, like saved connections.

Because we're using two extensions, the order in which they are enabled matters. The guacamole/guacamole Docker container automatically prioritizes the openid extension, which is what we want. This way users must sign in through Authentik, and the PostgreSQL database is only used for data storage, not authentication.

The extension-priority configuration option in the $GUACAMOLE_HOME/guacamole.properties file can be used to override the extension loading order. The EXTENSION_PRIORITY environment variable controls the same option when using the Docker container. However, this change is only in the upstream GitHub repository and hasn't made its way to the official Docker container yet. Despite this, I've defined it anyways (it doesn't hurt).

OpenID Connect parameters

We perform the necessary configuration using environment variables, which the Docker container will convert into configuration file entries.

The following environment variables are set for the guacamole container. Obviously, replace the URLs and client ID to match your setup.

      OPENID_AUTHORIZATION_ENDPOINT: "https://auth2.kasad.com/application/o/authorize/"
      OPENID_JWKS_ENDPOINT: "https://auth2.kasad.com/application/o/guacamole/jwks/"
      OPENID_ISSUER: "https://auth2.kasad.com/"
      OPENID_CLIENT_ID: "[redacted]"
      OPENID_REDIRECT_URI: "https://swag.kasad.com/guacamole/" # Trailing slash is important
      OPENID_USERNAME_CLAIM_TYPE: "preferred_username"

In Authentik, create a new Application and a new OpenID provider for Guacamole.

  • Set the slug for the application to guacamole, as that's what we've used in the variables above.
  • Set the redirect URI to the URL of the frontend (including the trailing slash).
  • Under Advanced protocol settings, set the Token validity to minutes=300. Guacamole will not accept any tokens with a lifetime greater than 300 minutes.
  • Also under Advanced protocol settings, set the Issuer mode to Same identifier is used for all providers, as that's what we've told Guacamole to expect.
The JWKS endpoint

If the JWKS endpoint is proxied behind Cloudflare (as ours is), it must have Cloudflare's Browser Integrity Check disabled. This can be accomplished by adding a Page Rule in the kasad.com zone for auth.kasad.com/jwks.json.

If this is not done, Guacamole will be prohibited from accessing the JWKS endpoint. In the container's logs, you'll find error messages about 403 Prohibited errors when trying to access the JWKS URL.

Creating an admin user

When Guacamole's database is initialized, a user is created with the username and password set to guacadmin. This user has administrator permissions on the Guacamole instance, meaning they have full control over all aspects. However, when logging in using SSO, this user is not accessible because the normal username/password login is not available.

To get around this, you have two options: (1) temporarily disable the SSO authentication extension, or (2) manually modify the Guacamole database.

I will not documennt option 2, but if you decide to go with that, see the System Permissions section of the Modifying data Manually documentation for Guacamole.

For option 1, you must first log in via SSO as the user you wish to turn into an administrator. This will create an entry for this user in the Guacamole database.

Next, disable the OpenID authentication extension by commenting out all the environment variable starting with OPENID_ in the Docker Compose file. Then re-create the stack using docker compose up -d.

Now log in to Guacamole as the guacadmin user. Then go to Settings > Users. Select the user you created in the first step and make them an administrator. While you're at it, change the password for the guacadmin user just in case.

Finally, revert the changes to the Docker Compose file and re-create the stack again. Now your SSO user should be an administrator.

Adding new users

When a user signs in to Guacamole using SSO for the first time, an entry will be created in Guacamole's database, but they will not be given any permissions. This means they cannot create connections on their own.

An administrator must grant them the necessary permissions once their account has been created.

Other Notes

Building from sources

I tried building the Guacamole frontend and backend containers myself from their sources. The backend container built fine, but the frontend container failed because of a missing libglib-2.0.so.0 library.

Unstable mobile keyboard input on Android

I've noticed that the software keyboard input mode doesn't work well on Android (at least in Brave Browser). Keystrokes are not sent through the connection until the backspace key is pressed. A somewhat-workaround for this is to type what you wish to send, then add an extra space, then hit backspace. It should result in the proper text being sent.

Upstream development

Guacamole doesn't seem to be very popular. Unfortunately, this means that upstream development is pretty slow. Bugs (even relatively severe ones) are not fixed quickly.

SSH host keys

Guacamole doesn't accept the standard format for SSH host keys (i.e. the one you'd find in ~/.ssh/known_hosts). However, it will not tell you that. If you attempt to use an SSH host key, it will simply inform you that "An internal error occurred."

guacamole/guacd image versions

The Dockerfile for guacamole/guacd is set up a certain way so that each build will pull the latest versions of the libraries it uses. A build bot automatically builds nightly versions of the guacamole/guacd image and uploads them to Docker Hub.

However, the latest tag still uses the latest tagged version of the guacd source code. So although it builds nightly, it does not use the latest Git revision. Instead, it uses the latest release (currently 1.4.0) and the newest libraries.

This means that the 1.4.0 and the latest images tags are the same in terms of guacd's functionality. The only way they differ is in what the underlying protocol libraries support.

VNC TLS failure

When connecting to a TigerVNC server using TLS transport security, a handshake error occurs. This only happens using the latest tag for the guacamole/guacd image. On tag 1.4.0, it works fine.

Sadly, using 1.4.0 is not a workaround for this because 1.4.0 lacks support for OpenSSH-style SSH keys. So we must choose between VNC with TLS and ED25519 SSH keys. I choose the latter.