Apache Guacamole 1.6.0 + Keycloak on AWS User Guide
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
guacdproxy 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
cloudimgand the OpenID Connect clientguacamolealready configured - PostgreSQL 16 holding Guacamole's connections and permissions and Keycloak's realm data, on
127.0.0.1only - 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
guacadminaccount 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 on127.0.0.1only. 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
Hostheader. Keycloak's issuer and every OpenID Connect redirect use that configured name, never a request header. - Real client addresses. nginx overwrites
X-Forwarded-Forwith the address it sees, and Tomcat and Keycloak trust forwarded headers from127.0.0.1only, 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-banbans 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
Originis 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 anIPAddressDenyrule for the metadata addresses (169.254.169.254, fd00:ec2::254, 168.63.129.16), and the AMI is registered IMDSv2 only.guacdcan 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.
guacdis 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
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- 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
- 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.
- Once the subscription is active, choose Launch your software.
- 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.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: allow SSH (22) from your own address and HTTPS (443) from the ranges your users connect from. Nothing else is needed.
- 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 realmcloudimgand 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:

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:

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.

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:

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:

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_URLexactly, or runsudo 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-banbanned 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-firstbootshows 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.