Networking AWS

Apache Guacamole 1.6.0 + Keycloak on AWS User Guide

| Product: Apache Guacamole 1.6.0 + Keycloak on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying Apache Guacamole 1.6.0 + Keycloak on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other VPN, proxy and remote access servers in the listing are described on the listing's guide hub.

Apache Guacamole is a clientless remote desktop gateway: your users open a web page and get a full RDP, VNC or SSH session to a server behind it, with nothing installed on their own machine. Keycloak is an identity and access management server. In this image the two are wired together: Guacamole sends every sign-in to a Keycloak realm on the same instance over OpenID Connect, so you manage users, password policy, brute-force lockout and multi-factor authentication in Keycloak, and grant connections in Guacamole.

What is on the image:

  • Apache Guacamole 1.6.0: the web application on Apache Tomcat 9.0.123 and the guacd proxy daemon, built by cloudimg from the published 1.6.0 source, with RDP, VNC and SSH support
  • Keycloak 26.8.1 in production mode, with a realm cloudimg and the OpenID Connect client guacamole already configured
  • PostgreSQL 16 holding Guacamole's connections and permissions and Keycloak's realm data, on 127.0.0.1 only
  • nginx serving HTTPS on port 443 only, with a certificate generated for your instance on first boot
  • The Guacamole extensions for PostgreSQL, OpenID Connect and brute-force protection (guacamole-auth-ban)
  • A self-test, guacamole-keycloak-selftest, and a helper to move the instance to a DNS name or Elastic IP, guacamole-keycloak-set-host

Default user: ubuntu · Architecture: x86_64 · Root volume: 16 GiB gp3

Security posture

  • No default credential. Guacamole's published guacadmin account is never created, and Keycloak has no administrator until your instance's first boot creates one. First boot generates the Keycloak administrator password, the single sign-on (SSO) user's password, both database passwords and the TLS key for this instance only, and writes the two sign-in passwords to /root/guacamole-keycloak-credentials.txt (root only).
  • No shared database. The image carries no PostgreSQL data at all: first boot creates a brand-new database cluster on your instance, so no password hash or identifier is shared with any other instance.
  • One public port. nginx listens on TCP 443. Tomcat (8080), Keycloak (8081), guacd (4822) and PostgreSQL (5432) listen on 127.0.0.1 only. The listing's security group opens SSH (22) and HTTPS (443).
  • Fixed host name. nginx answers only to the name this instance is configured for (its public IPv4 address, or the DNS name you set in Step 9) and closes the connection for any other Host header. Keycloak's issuer and every OpenID Connect redirect use that configured name, never a request header.
  • Real client addresses. nginx overwrites X-Forwarded-For with the address it sees, and Tomcat and Keycloak trust forwarded headers from 127.0.0.1 only, so Guacamole's logs and its brute-force ban see the real client and a forged header changes nothing.
  • Brute-force protection on both halves. guacamole-auth-ban bans an address for 5 minutes after 5 failed Guacamole logins, and Keycloak's brute-force detection is on for both realms. Self-registration is off.
  • Cross-site protection. A Guacamole WebSocket tunnel or a state-changing request whose Origin is not this instance's own address is refused with 403, so another web site cannot open a session with your users' credentials.
  • No SSRF to instance credentials. guacd, Tomcat, Keycloak, PostgreSQL and nginx each carry an IPAddressDeny rule for the metadata addresses (169.254.169.254, fd00:ec2::254, 168.63.129.16), and the AMI is registered IMDSv2 only. guacd can still reach every other address, so your RDP, VNC and SSH targets work.
  • Patched components. Keycloak 26.8.1 and Tomcat 9.0.123 are the current releases. The Guacamole 1.6.0 web application bundles Jackson 2.19.0 and Logback 1.3.15, which have published security fixes; this image replaces exactly those libraries with Jackson 2.21.8 and Logback 1.3.16. guacd is built against Ubuntu's supported FreeRDP 3 libraries, and no build tools ship.
  • Fails closed. Every service starts only after first boot has written this instance's configuration and a ready marker, and first boot finishes only after it has proved a full single sign-on round trip. If it cannot, every service stays stopped and first boot starts over on the next boot.
  • Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance.

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. The public IP ranges your users connect from, and network access from the instance to the servers you want to reach (security groups, VPC routing or a VPN)

Recommended instance type: m5.large (2 vCPU, 8 GiB). Keycloak, Tomcat and PostgreSQL use about 1.5 GiB at rest; add memory for many concurrent sessions.

Connecting to your instance

Version Operating system SSH user
Apache Guacamole 1.6.0 + Keycloak on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Once the subscription is active, choose Launch your software.
  3. Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Apache Guacamole 1.6.0 + Keycloak on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: allow SSH (22) from your own address and HTTPS (443) from the ranges your users connect from. Nothing else is needed.
  6. Choose your VPC and a public subnet (or attach an Elastic IP later), leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR ranges with your own:

SG=$(aws ec2 create-security-group --group-name guacamole-keycloak \
  --description "Apache Guacamole + Keycloak" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22  --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr <your-client-cidr>
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=guacamole-keycloak}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required.

Step 2: Connect and read the sign-in details

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot takes two to three minutes: it creates the database, starts Keycloak for the first time, creates the realm, client and users, and then proves that a sign-in works end to end. The details are in /root/guacamole-keycloak-credentials.txt, readable only by root. This command prints everything except the two password lines:

sudo grep -vE '^(SSO_ADMIN_PASSWORD|KEYCLOAK_ADMIN_PASSWORD)=' /root/guacamole-keycloak-credentials.txt
# Apache Guacamole 1.6.0 + Keycloak 26.8.1 - generated on first boot by guacamole-keycloak-firstboot.service.
# Every value below is unique to this instance; nothing was baked into the image.

GUACAMOLE_URL=https://44.203.97.41/
KEYCLOAK_ADMIN_URL=https://44.203.97.41/auth/admin/

# Sign in to Guacamole with this identity (Guacamole hands you to Keycloak; Keycloak checks the password).
SSO_ADMIN_USER=cloudimg-admin

# The Keycloak administration console (master realm): users, login policy, MFA, identity providers.
KEYCLOAK_ADMIN_USER=cloudimg

# The certificate is self-signed and unique to this instance (browsers warn on the first visit).
TLS_CERT_SHA256=68:77:20:0E:F3:3A:61:58:5C:AA:BA:C0:E8:A0:5E:22:2C:A3:3C:CA:60:AA:AE:20:F4:B5:A6:68:03:FB:CA:7F

# New DNS name or Elastic IP:  sudo guacamole-keycloak-set-host <name-or-ip>
# Self-test:                   sudo guacamole-keycloak-selftest
# Guide: https://www.cloudimg.co.uk/guides/apache-guacamole-1-6-0-keycloak-on-ubuntu-24-04-aws/

There are two identities:

  • cloudimg-admin (SSO_ADMIN_USER) signs in to Guacamole. It lives in the Keycloak realm cloudimg and is a Guacamole administrator.
  • cloudimg (KEYCLOAK_ADMIN_USER) signs in to the Keycloak administration console (master realm), where you manage users, policies and identity providers.

Print a password only when you need it, for example sudo sed -n 's/^SSO_ADMIN_PASSWORD=//p' /root/guacamole-keycloak-credentials.txt.

Step 3: Confirm the stack is running

All five services and the first-boot unit are active, and only SSH and HTTPS listen on a public address:

systemctl is-active postgresql@16-main.service keycloak.service guacd.service tomcat.service nginx.service guacamole-keycloak-firstboot.service
/usr/local/sbin/guacd -v
cat /opt/keycloak/version.txt
sudo ss -Htln | awk '{print $4}' | sort -u
active
active
active
active
active
active
Guacamole proxy daemon (guacd) version 1.6.0
Keycloak - Version 26.8.1
0.0.0.0:22
0.0.0.0:443
127.0.0.1:4822
127.0.0.1:5432
127.0.0.53%lo:53
127.0.0.54:53
[::]:22
[::]:443
[::ffff:127.0.0.1]:8080
[::ffff:127.0.0.1]:8081

127.0.0.53 and 127.0.0.54 are Ubuntu's local DNS resolver.

Step 4: Run the self-test

The self-test checks that every service is active, that guacd loads its RDP, VNC and SSH plugins, and then performs the whole single sign-on round trip through nginx exactly as a browser does: Guacamole sends the sign-in to Keycloak, a wrong password is refused, the SSO user's password is accepted, and Guacamole opens a session for cloudimg-admin. It also proves that Keycloak refuses admin/admin and that no guacadmin account exists. No password is printed:

sudo guacamole-keycloak-selftest
services: postgresql@16-main, keycloak, guacd, tomcat and nginx are active
guacd plugins: rdp(88 params, VERSION_1_5_0) vnc(49 params, VERSION_1_5_0) ssh(42 params, VERSION_1_5_0)
single sign-on: Guacamole -> Keycloak realm 'cloudimg' -> per-instance password -> Guacamole session (wrong password refused)
defaults: Keycloak admin/admin refused; the guacadmin account does not exist
SELFTEST_OK

Step 5: Sign in through the browser

Open GUACAMOLE_URL from Step 2 (for example https://44.203.97.41/). The certificate is self-signed, so the browser warns on the first visit; compare the fingerprint it shows with TLS_CERT_SHA256 and continue (Step 10 replaces it with your own). Guacamole sends you straight to the Keycloak sign-in page of the realm:

Keycloak sign-in page for Apache Guacamole

Sign in as cloudimg-admin with SSO_ADMIN_PASSWORD. Keycloak returns you to Guacamole, signed in. The home page lists your connections, empty until you add one:

Guacamole home page after single sign-on

Step 6: Add your first connection

Open the user menu (top right), choose Settings, then Connections and New Connection. Give it a name, choose the protocol and fill in the target's Hostname and Port under Parameters > Network (3389 for RDP, 5900 for VNC, 22 for SSH), plus the credentials under Authentication if you want Guacamole to sign in for the user. Save, and the connection appears on the home page.

Guacamole new connection form with RDP selected

This image's guacd supports RDP, VNC and SSH. The form also lists Telnet and Kubernetes, which this build does not include (Telnet is unencrypted). The target must be reachable from the instance: allow RDP, VNC or SSH from the Guacamole instance's security group on the target, not from the internet.

Settings > Users shows every Guacamole identity. A user who signs in through Keycloak for the first time gets an account here automatically, with no connections until you grant them:

Guacamole users list

Step 7: Add users in Keycloak

Users sign in with Keycloak, so you create them there. Open KEYCLOAK_ADMIN_URL (for example https://44.203.97.41/auth/admin/) and sign in as cloudimg with KEYCLOAK_ADMIN_PASSWORD. Switch to the Apache Guacamole realm (cloudimg) with Manage realms, open Users, choose Add user, then set a password on the user's Credentials tab. The realm requires passwords of at least 12 characters that differ from the user name:

Keycloak users in the cloudimg realm

When the new user first signs in to Guacamole, Guacamole creates their account with no permissions. As cloudimg-admin, open Settings > Users, select the user and tick the connections (or connection groups) they may use. To make someone a Guacamole administrator, grant Administer system on the same page.

Keycloak is also where you add multi-factor authentication (realm cloudimg > Authentication > set OTP Form to Required in the browser flow, or give users the Configure OTP required action), connect your corporate directory (User federation: LDAP or Active Directory) or another identity provider (Identity providers: SAML or OpenID Connect).

Step 8: Brute-force protection

Guacamole loads the brute-force extension next to the PostgreSQL and OpenID Connect extensions, and its settings are in guacamole.properties (5 failed logins from one address ban that address for 300 seconds). Keycloak's own brute-force detection is on for the master and cloudimg realms (Realm settings > Security defenses):

sudo ls /etc/guacamole/extensions
sudo grep -E '^(extension-priority|ban-)' /etc/guacamole/guacamole.properties
guacamole-auth-ban-1.6.0.jar
guacamole-auth-jdbc-postgresql-1.6.0.jar
guacamole-auth-sso-openid-1.6.0.jar
extension-priority: openid
ban-max-invalid-attempts:      5
ban-address-duration:          300

Because nginx passes the real client address and Tomcat trusts it from 127.0.0.1 only, the ban applies to the attacker's address, not to everyone behind the proxy.

Step 9: Use a DNS name or an Elastic IP

Sign-in redirects, the Keycloak issuer, the certificate and nginx all name one host. First boot uses the instance's public IPv4 address. If you stop and start the instance without an Elastic IP, the public address changes and sign-in stops working until you run the helper again (the login message warns you when this happens). With no argument, guacamole-keycloak-set-host reads the current address from the instance metadata:

sudo guacamole-keycloak-set-host | tail -3
Host changed from 44.203.97.41 to 44.203.97.41: Guacamole https://44.203.97.41/  Keycloak admin https://44.203.97.41/auth/admin/
Certificate SHA-256: 35:97:DE:E8:01:55:42:BE:16:00:54:81:14:1A:D5:B1:85:2C:48:F8:46:36:FE:D1:55:1F:56:5C:E7:6B:00:4B
SET_HOST_OK

To use your own name, point a DNS record at the instance (attach an Elastic IP first so the address never changes) and pass the name: sudo guacamole-keycloak-set-host guac.example.com. The helper updates the Keycloak client, Keycloak's hostname, Guacamole's OpenID Connect settings, nginx and the URLs in the credentials file, issues a new self-signed certificate for that name, restarts the services and proves sign-in again. If nginx or Keycloak rejects the change it restores the previous settings. It signs in to Keycloak with KEYCLOAK_ADMIN_USER and KEYCLOAK_ADMIN_PASSWORD from the credentials file, so if you change that password, update the file too.

Step 10: Use your own certificate

After Step 9 has set the name, copy your certificate (full chain) and key over the self-signed pair and reload nginx:

sudo install -m 0644 fullchain.pem /etc/nginx/tls/server.crt
sudo install -m 0600 privkey.pem /etc/nginx/tls/server.key
sudo nginx -t && sudo systemctl reload nginx

Running guacamole-keycloak-set-host again later issues a new self-signed certificate, so copy your certificate back afterwards. Alternatively, put an Application Load Balancer with an AWS Certificate Manager certificate in front of port 443 and keep the self-signed certificate between the load balancer and the instance.

Step 11: Metadata access is blocked

Every service of the stack carries an IPAddressDeny rule for the metadata addresses, so a connection someone configures in Guacamole, a Keycloak identity-provider URL or anything else a user can influence cannot reach the instance's credentials:

for u in postgresql@16-main keycloak guacd tomcat nginx; do printf '%-20s %s\n' "$u" "$(systemctl show -p IPAddressDeny --value "$u.service")"; done
postgresql@16-main   168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
keycloak             168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
guacd                168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
tomcat               168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
nginx                168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32

Step 12: How the fail-closed start works

Every service is gated on /var/lib/cloudimg/guacamole-keycloak.ready, which first boot writes only after this instance's database, configuration and certificate exist. The files holding secrets are readable only by root and the one service that needs them:

systemctl cat keycloak.service nginx.service | grep '^ConditionPathExists='
sudo stat -c '%a %U:%G %n' /root/guacamole-keycloak-credentials.txt /etc/guacamole/guacamole.properties /etc/keycloak/keycloak.env /etc/nginx/tls/server.key
ConditionPathExists=/var/lib/cloudimg/guacamole-keycloak.ready
ConditionPathExists=/var/lib/cloudimg/guacamole-keycloak.ready
600 root:root /root/guacamole-keycloak-credentials.txt
640 root:tomcat /etc/guacamole/guacamole.properties
640 root:keycloak /etc/keycloak/keycloak.env
600 root:root /etc/nginx/tls/server.key

Network ports

Port Listens on Purpose
22/tcp all addresses SSH administration (restrict to your own address)
443/tcp all addresses Guacamole at / and Keycloak at /auth/ over HTTPS
8080/tcp 127.0.0.1 Tomcat (Guacamole web application), behind nginx
8081/tcp 127.0.0.1 Keycloak, behind nginx
4822/tcp 127.0.0.1 guacd
5432/tcp 127.0.0.1 PostgreSQL 16

Port 80 is not used: give your users the https:// address.

Where things live

Path What it is
/root/guacamole-keycloak-credentials.txt Sign-in URLs and the two per-instance passwords
/etc/guacamole/guacamole.properties Guacamole settings (database, OpenID Connect, brute-force ban)
/etc/keycloak/keycloak.env Keycloak runtime settings (database, hostname, proxy)
/etc/nginx/sites-available/cloudimg-guacamole-keycloak The HTTPS site; /etc/nginx/tls/ holds the certificate
/opt/tomcat/, /opt/keycloak/ Tomcat 9.0.123 with the Guacamole web application; Keycloak 26.8.1
/usr/local/sbin/guacd guacd 1.6.0
/var/lib/postgresql/16/main PostgreSQL data (created on first boot)

Logs are in the journal: journalctl -u tomcat (Guacamole sign-ins and connections), journalctl -u keycloak, journalctl -u guacd, and journalctl -u guacamole-keycloak-firstboot for first boot.

Troubleshooting

  • The browser shows "connection closed" or nothing at all: you used a name nginx is not configured for. Use GUACAMOLE_URL exactly, or run sudo guacamole-keycloak-set-host (Step 9).
  • "Invalid redirect URI" or a sign-in loop: the configured host no longer matches the instance's address (stopped and started without an Elastic IP). Run sudo guacamole-keycloak-set-host.
  • "Too many failed authentication attempts": guacamole-auth-ban banned your address for 5 minutes after repeated failures. Wait, or restart Tomcat to clear the list.
  • A connection fails to start: check that the target allows RDP, VNC or SSH from the Guacamole instance and look at journalctl -u guacd.
  • First boot did not finish: journalctl -u guacamole-keycloak-firstboot shows the step that failed; every service stays stopped until it succeeds, and a reboot retries it.

Security recommendations

  • Keep TCP 443 open only to the ranges your users connect from, and TCP 22 only to your management addresses.
  • Turn on multi-factor authentication in Keycloak for every user (Step 7), especially administrators.
  • Give each person their own Keycloak user and grant only the connections they need in Guacamole.
  • Attach an Elastic IP and a DNS name, and use a certificate your users' browsers already trust (Steps 9 and 10).
  • Leave unattended security upgrades on, and reboot after kernel updates.

Support and licensing

Apache Guacamole and Apache Tomcat are projects of the Apache Software Foundation, and Keycloak is a Cloud Native Computing Foundation project; all three are distributed under the Apache License 2.0. This image builds the unmodified Guacamole 1.6.0 server source, ships the Guacamole 1.6.0 web application with its Jackson and Logback libraries updated as described above, and ships the unmodified Keycloak 26.8.1 and Tomcat 9.0.123 releases. cloudimg provides the packaging, the integration, the secure defaults, the per-instance credentials and certificate, the metadata block, the fail-closed start, the self-test and 24/7 support.

cloudimg is not affiliated with or endorsed by the Apache Software Foundation, the Keycloak project or the Cloud Native Computing Foundation. Apache, Apache Guacamole, Guacamole and Apache Tomcat are trademarks of the Apache Software Foundation; Keycloak is a trademark of its respective owner. They are used here only to identify the software.

For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.