Guacamole - Remote Access
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.
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:
Guacamole is published behind the Secure Web Application Gateway at guac.kasad.com. 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
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
This container uses the latest upstream sources and is published on Docker Hub as
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 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.
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.
Guacamole's frontend server utilizes extensions to provide authentication backends. We use the
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.
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://guac.kasad.com/" # 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
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
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
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.
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
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.
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.
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.
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.