Chapter 4. Proxying Guacamole

Like most web applications, Guacamole can be placed behind a reverse proxy. For production deployments of Guacamole, this is highly recommended. It provides flexibility and, if your proxy is properly configured for SSL, encryption.

Proxying isolates privileged operations within native applications that can safely drop those privileges when no longer needed, using Java only for unprivileged tasks. On Linux and UNIX systems, a process must be running with root privileges to listen on any port under 1024, including the standard HTTP and HTTPS ports (80 and 443 respectively). If the servlet container instead listens on a higher port, such as the default port 8080, it can run as a reduced-privilege user, allowing the reverse proxy to bear the burden of root privileges. As a native application, the reverse proxy can make system calls to safely drop root privileges once the port is open; a Java application like Tomcat cannot do this.

Preparing your servlet container

Your servlet container is most likely already configured to listen for HTTP connections on port 8080 as this is the default. If this is the case, and you can already access Guacamole over port 8080 from a web browser, you need not make any further changes to its configuration.

If you have changed this, perhaps with the intent of proxying Guacamole over AJP, change it back. Using Guacamole over AJP is unsupported as it is known to cause problems, namely:

  1. WebSocket will not work over AJP, forcing Guacamole to fallback to HTTP, possibly resulting in reduced performance.

  2. Apache 2.4.3 and older does not support the HTTP PATCH method over AJP, preventing the Guacamole management interface from functioning properly.

The connector entry within conf/server.xml should look like this:

<Connector port="8080" protocol="HTTP/1.1" 
           connectionTimeout="20000"
           URIEncoding="UTF-8"
           redirectPort="8443" />

Be sure to specify the URIEncoding="UTF-8" attribute as above to ensure that connection names, user names, etc. are properly received by the web application. If you will be creating connections that have Cyrillic, Chinese, Japanese, or other non-Latin characters in their names or parameter values, this attribute is required.

Nginx

Nginx can be used as a reverse proxy, and supports WebSocket out-of-the-box since version 1.3. Both Apache and Nginx require some additional configuration for proxying of WebSocket to work properly.

Proxying Guacamole

Nginx does support WebSocket for proxying, but requires that the "Connection" and "Upgrade" HTTP headers are set explicitly due to the nature of the WebSocket protocol. From the Nginx documentation:

NGINX supports WebSocket by allowing a tunnel to be set up between a client and a back-end server. For NGINX to send the Upgrade request from the client to the back-end server, Upgrade and Connection headers must be set explicitly. ...

The proxy configuration belongs within a dedicated location block, declaring the backend hosting Guacamole and explicitly specifying the "Connection" and "Upgrade" headers mentioned earlier:

location /guacamole/ {
    proxy_pass http://HOSTNAME:8080/guacamole/;
    proxy_buffering off;
    proxy_http_version 1.1;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;
    access_log off;
}

Here, HOSTNAME is the hostname or IP address of the machine hosting your servlet container, and 8080 is the port that servlet container is configured to use. You will need to replace these values with the correct values for your server.

Important

Do not forget to specify "proxy_buffering off".

Most proxies, including Nginx, will buffer all data sent over the connection, waiting until the connection is closed before sending that data to the client. As Guacamole's HTTP tunnel relies on streaming data to the client over an open connection, excessive buffering will effectively block Guacamole connections, rendering Guacamole useless.

If the option "proxy_buffering off" is not specified, Guacamole may not work.

Changing the path

If you wish to serve Guacamole through Nginx under a path other than /guacamole/, the configuration will need to be altered slightly to take cookies into account. Although Guacamole does not rely on receipt of cookies in general, cookies are required for the proper operation of the HTTP tunnel. If the HTTP tunnel is used, and cookies cannot be set, users may be unexpectedly denied access to their connections.

Regardless of the location specified for the proxy, cookies set by Guacamole will be set using its own absolute path within the backend (/guacamole/). If this path differs from that used by Nginx, the path in the cookie needs to be modified using proxy_cookie_path:

location /new-path/ {
    proxy_pass http://HOSTNAME:8080/guacamole/;
    proxy_buffering off;
    proxy_http_version 1.1;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;
    proxy_cookie_path /guacamole/ /new-path/;
    access_log off;
}

Apache and mod_proxy

Apache supports reverse proxy configurations through mod_proxy. Apache 2.4.5 and later also support proxying of WebSocket through a sub-module called mod_proxy_wstunnel. Both of these modules will need to be enabled for proxying of Guacamole to work properly.

Lacking mod_proxy_wstunnel, it is still possible to proxy Guacamole, but Guacamole will be unable to use WebSocket. It will instead fallback to using the HTTP tunnel, resulting in reduced performance.

Proxying Guacamole

Configuring Apache to proxy HTTP requests requires using the ProxyPass and ProxyPassReverse directives, which are provided by the mod_proxy module. These directives describe how HTTP traffic should be routed to the web server behind the proxy:

<Location /guacamole/>
    Order allow,deny
    Allow from all
    ProxyPass http://HOSTNAME:8080/guacamole/ flushpackets=on
    ProxyPassReverse http://HOSTNAME:8080/guacamole/
</Location>

Here, HOSTNAME is the hostname or IP address of the machine hosting your servlet container, and 8080 is the port that servlet container is configured to use. You will need to replace these values with the correct values for your server.

Important

Do not forget the flushpackets=on option.

Most proxies, including mod_proxy, will buffer all data sent over the connection, waiting until the connection is closed before sending that data to the client. As Guacamole's HTTP tunnel relies on streaming data to the client over an open connection, excessive buffering will effectively block Guacamole connections, rendering Guacamole useless.

If the option flushpackets=on is not specified, Guacamole may not work.

Proxying the WebSocket tunnel

Apache will not automatically proxy WebSocket connections, but you can proxy them separately with Apache 2.4.5 and later using mod_proxy_wstunnel. After enabling mod_proxy_wstunnel a secondary Location section can be added which explicitly proxies the Guacamole WebSocket tunnel, located at /guacamole/websocket-tunnel:

<Location /guacamole/websocket-tunnel>
    Order allow,deny
    Allow from all
    ProxyPass ws://HOSTNAME:8080/guacamole/websocket-tunnel
    ProxyPassReverse ws://HOSTNAME:8080/guacamole/websocket-tunnel
</Location>

Lacking this, Guacamole will still work by using normal HTTP, but network latency will be more pronounced with respect to user input, and performance may be lower.

Important

The Location section for /guacamole/websocket-tunnel must be placed after the Location section for the rest of Guacamole.

Apache evaluates all Location sections, giving priority to the last section that matches. If the /guacamole/websocket-tunnel section comes first, the section for /guacamole/ will match instead, and WebSocket will not be proxied correctly.

Changing the path

If you wish to serve Guacamole through Apache under a path other than /guacamole/, the configuration required for Apache will be slightly different than the examples above due to cookies.

Guacamole does not rely on receipt of cookies for tracking whether a user is logged in, but cookies are required for the proper operation of the HTTP tunnel. If the HTTP tunnel is used, and cookies cannot be set, users will be unexpectedly denied access to connections they legitimately should have access to.

Cookies are set using the absolute path of the web application (/guacamole/). If this path differs from that used by Apache, the path in the cookie needs to be modified using the ProxyPassReverseCookiePath directive:

<Location /new-path/>
    Order allow,deny
    Allow from all
    ProxyPass http://HOSTNAME:8080/guacamole/ flushpackets=on
    ProxyPassReverse http://HOSTNAME:8080/guacamole/
    ProxyPassReverseCookiePath /guacamole/ /new-path/
</Location>

<Location /new-path/websocket-tunnel>
    Order allow,deny
    Allow from all
    ProxyPass ws://HOSTNAME:8080/guacamole/websocket-tunnel
    ProxyPassReverse ws://HOSTNAME:8080/guacamole/websocket-tunnel
</Location>

This directive is not needed for the WebSocket section, as it is not applicable. Cookies are only used by Guacamole within the HTTP tunnel.

Disabling logging of tunnel requests

If WebSocket is unavailable, Guacamole will fallback to using an HTTP-based tunnel. The Guacamole HTTP tunnel works by transferring a continuous stream of data over multiple short-lived streams, each associated with a separate HTTP request. By default, Apache will log each of these requests, resulting in a rather bloated access log.

There is little value in a log file filled with identical tunnel requests, so it is recommended to explicitly disable logging of those requests. Apache does provide a means of matching URL patterns and setting environment variables based on whether the URL matches. Logging can then be restricted to requests which lack this environment variable:

SetEnvIf Request_URI "^/guacamole/tunnel" dontlog
CustomLog  /var/log/apache2/guac.log common env=!dontlog

Note that if you are serving Guacamole under a path different from /guacamole/, you will need to change the value of Request_URI above accordingly.