Complete Apache Guacamole 1.6.0 Installation on Ubuntu Server
Stack covered: Guacamole 1.6.0 · MariaDB · Apache Tomcat 9 · Keycloak 26 (SSO OpenID Connect) · HashiCorp Vault · Python (automation scripts)
Table of Contents
- Compiling and installing guacamole-server
- Installing the web application (Tomcat + .war)
- Configuring the MariaDB database
- Configuring Guacamole
- Session recording
- SSO with Keycloak (OpenID Connect)
- Automatic Keycloak → Guacamole synchronization
- Secret management with HashiCorp Vault
- Useful scripts
- References
1. Compiling and installing guacamole-server
1.1 Install build dependencies
sudo apt update
sudo apt install build-essential libcairo2-dev libjpeg-turbo8-dev libpng-dev \
libtool-bin libossp-uuid-dev libvncserver-dev freerdp2-dev libssh2-1-dev \
libtelnet-dev libwebsockets-dev libpulse-dev libvorbis-dev libwebp-dev \
libssl-dev libpango1.0-dev libswscale-dev libavcodec-dev libavutil-dev \
libavformat-dev
1.2 Download and extract the sources
Option A - from Apache archives (recommended):
wget https://archive.apache.org/dist/guacamole/1.6.0/source/guacamole-server-1.6.0.tar.gz
tar -xvzf guacamole-server-1.6.0.tar.gz
mv guacamole-server-1.6.0 guacamole-server
cd guacamole-server
Option B - from GitHub:
git clone https://github.com/apache/guacamole-server.git
cd guacamole-server
1.3 Compile and install
autoreconf -fi
./configure --with-systemd-dir=/usr/local/lib/systemd/system
make
sudo make install
sudo ldconfig
Make sure the expected libraries and protocols are detected before running make (check the Library status and Protocol support sections in the ./configure output).
1.4 Start and enable guacd
sudo systemctl daemon-reload
sudo systemctl start guacd
sudo systemctl enable guacd
sudo systemctl status guacd
Verify that guacd is listening on port 4822:
sudo ss -lnpt | grep guacd
# LISTEN 0 5 0.0.0.0:4822 0.0.0.0:* users:(("guacd",...))
Common issue: if guacd is only listening on
::1(IPv6 loopback) instead of0.0.0.0, Tomcat will not be able to reach it. Create the file/etc/guacamole/guacd.conf:[server] bind_host = 0.0.0.0 bind_port = 4822Then restart:
sudo systemctl restart guacd
2. Installing the web application (Tomcat + .war)
2.1 Install Apache Tomcat 9
sudo apt install tomcat9 tomcat9-admin tomcat9-common tomcat9-user
Tomcat listens on port 8080:
sudo ss -lnpt | grep java
If another service is already using port 8080, reconfigure it to a different port before starting Tomcat.
2.2 Deploy the Guacamole .war file
wget https://downloads.apache.org/guacamole/1.6.0/binary/guacamole-1.6.0.war
sudo mv guacamole-1.6.0.war /var/lib/tomcat9/webapps/guacamole.war
sudo systemctl restart tomcat9
sudo systemctl enable tomcat9
The interface is accessible at: http://<IP>:8080/guacamole
2.3 Create configuration directories
sudo mkdir /etc/guacamole
sudo mkdir /etc/guacamole/{extensions,lib}
3. Configuring the MariaDB database
3.1 Install and secure MariaDB
sudo apt install mariadb-server
sudo systemctl start mariadb
sudo systemctl enable mariadb
sudo mysql_secure_installation
Answer as follows during mysql_secure_installation:
| Question | Answer |
|---|---|
| Switch to unix_socket authentication | N |
| Change the root password | Y (strong password) |
| Remove anonymous users | Y |
| Disallow root login remotely | Y |
| Remove test database | Y |
| Reload privilege tables | Y |
3.2 Create the database and user
mysql -u root -p
CREATE DATABASE guacamole_db;
CREATE USER 'guacamole_user'@'localhost' IDENTIFIED BY 'YourPassword!';
GRANT SELECT,INSERT,UPDATE,DELETE ON guacamole_db.* TO 'guacamole_user'@'localhost';
FLUSH PRIVILEGES;
quit;
3.3 Install the MySQL JDBC module for Guacamole
wget https://downloads.apache.org/guacamole/1.6.0/binary/guacamole-auth-jdbc-1.6.0.tar.gz
tar -xvf guacamole-auth-jdbc-1.6.0.tar.gz
# Copy the extension into Guacamole
sudo cp guacamole-auth-jdbc-1.6.0/mysql/guacamole-auth-jdbc-mysql-1.6.0.jar \
/etc/guacamole/extensions/
# Import the SQL schema
cat guacamole-auth-jdbc-1.6.0/mysql/schema/*.sql | mysql -u root -p guacamole_db
3.4 Install the MySQL JDBC connector
sudo apt install libmariadb-java
wget https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/8.0.33/mysql-connector-j-8.0.33.jar
sudo mkdir -p /etc/guacamole/lib
sudo mv mysql-connector-j-8.0.33.jar /etc/guacamole/lib/
sudo systemctl restart mariadb
4. Configuring Guacamole
4.1 Create guacamole.properties
sudo nano /etc/guacamole/guacamole.properties
# Connection to the guacd daemon
guacd-hostname: 0.0.0.0
guacd-port: 4822
# MySQL/MariaDB database
mysql-hostname: localhost
mysql-port: 3306
mysql-database: guacamole_db
mysql-username: guacamole_user
mysql-password: YourPassword!
# Recording storage path (required for history recording)
recording-search-path: /var/lib/guacamole/recordings
enable-shared-connections: true
4.2 Restart services
sudo systemctl restart tomcat9 guacd
sudo systemctl status tomcat9 guacd
Access the interface at: http://<IP>:8080/guacamole
Default credentials: guacadmin / guacadmin (change immediately)
Best practice: create a new admin user from the Users tab, then delete the default
guacadminaccount.
5. Session recording
5.1 Install the history-recording-storage extension
wget https://archive.apache.org/dist/guacamole/1.6.0/binary/guacamole-history-recording-storage-1.6.0.tar.gz
tar -xzf guacamole-history-recording-storage-1.6.0.tar.gz
sudo cp guacamole-history-recording-storage-1.6.0/guacamole-history-recording-storage-1.6.0.jar \
/etc/guacamole/extensions/
5.2 Create and configure recording directories
sudo mkdir -p /var/lib/guacamole/recordings
sudo mkdir -p /var/lib/guacamole/typescripts
# guacd (daemon) writes the .guac files
sudo chown -R daemon:daemon /var/lib/guacamole/recordings
sudo chown -R daemon:daemon /var/lib/guacamole/typescripts
# Add tomcat to the daemon group for read access (playback)
sudo usermod -aG daemon tomcat
sudo chmod -R 750 /var/lib/guacamole/recordings
sudo chmod -R 750 /var/lib/guacamole/typescripts
sudo systemctl restart guacd tomcat9
5.3 Fix the history_uuid column (if empty in history)
If the Logs column remains empty in Guacamole's history, the guacamole_connection_history table is missing the history_uuid column. Apply the following patch:
sudo mysql -u root -p guacamole_db < \
guacamole-auth-jdbc-1.6.0/mysql/schema/upgrade/upgrade-pre-1.6.0.sql
If the column is still missing, add it manually and create a trigger:
mysql -u root -p guacamole_db -e \
"ALTER TABLE guacamole_connection_history ADD COLUMN history_uuid CHAR(36) DEFAULT NULL;"
mysql -u root -p guacamole_db -e \
"CREATE TRIGGER guacamole_connection_history_uuid
BEFORE INSERT ON guacamole_connection_history
FOR EACH ROW SET NEW.history_uuid = UUID();"
5.4 Configuring a connection with recording
In the Guacamole connection parameters, fill in:
| Parameter | Value |
|---|---|
| Recording path | /var/lib/guacamole/recordings |
| Recording name | ${GUAC_USERNAME} |
| Create recording path automatically | |
| Include keyboard events | |
| Typescript path | /var/lib/guacamole/typescripts |
6. SSO with Keycloak (OpenID Connect)
6.1 Install Keycloak 26
# Prerequisite: Java 21
sudo apt update
sudo apt install -y openjdk-21-jdk
java -version
# Download Keycloak
wget https://github.com/keycloak/keycloak/releases/download/26.1.4/keycloak-26.1.4.tar.gz
tar -xzf keycloak-26.1.4.tar.gz
sudo mv keycloak-26.1.4 /opt/keycloak
# Create a dedicated system user
sudo useradd -r -d /opt/keycloak -s /sbin/nologin keycloak
sudo chown -R keycloak:keycloak /opt/keycloak
6.2 Start Keycloak (dev mode for testing)
Terminal 1 — start Keycloak:
sudo -u keycloak KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=YourPassword! \
/opt/keycloak/bin/kc.sh start-dev --http-port=8180
Terminal 2 — configure via kcadm:
/opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8180 \
--realm master \
--user admin \
--password YourPassword!
# Disable mandatory SSL for testing
/opt/keycloak/bin/kcadm.sh update realms/master -s sslRequired=NONE
/opt/keycloak/bin/kcadm.sh update realms/guacamole -s sslRequired=NONE
6.3 Create the Realm and the OIDC Client
From the Keycloak interface (http://<IP>:8180):
-
Create a realm named
guacamole -
In that realm, create a client:
- Client type: OpenID Connect
- Client ID:
guacamole - Client authentication: Off (public client)
- Authentication flow: Standard flow + Implicit flow
-
In Login settings, fill in (replace
<IP>with the server IP):
| Field | Value |
|---|---|
| Root URL | http://<IP>:8080/guacamole |
| Home URL | http://<IP>:8080/guacamole |
| Valid redirect URIs | http://<IP>:8080/guacamole/* |
| Valid post logout redirect URIs | http://<IP>:8080/guacamole |
| Web origins | http://<IP>:8080 |
6.4 Create a test user
In the guacamole realm → Users → Create new user:
- Username:
testuser - Email:
test@test.com - First/Last name:
Test User
Credentials tab → Set password → Test1234! (Temporary: Off)
6.5 Install the OpenID extension in Guacamole
wget https://downloads.apache.org/guacamole/1.6.0/binary/guacamole-auth-sso-1.6.0.tar.gz
tar -xzf guacamole-auth-sso-1.6.0.tar.gz
sudo cp guacamole-auth-sso-1.6.0/openid/guacamole-auth-sso-openid-1.6.0.jar \
/etc/guacamole/extensions/
Add the following to /etc/guacamole/guacamole.properties:
openid-authorization-endpoint: http://<IP>:8180/realms/guacamole/protocol/openid-connect/auth
openid-jwks-endpoint: http://<IP>:8180/realms/guacamole/protocol/openid-connect/certs
openid-issuer: http://<IP>:8180/realms/guacamole
openid-client-id: guacamole
openid-redirect-uri: http://<IP>:8080/guacamole
openid-username-claim-type: preferred_username
sudo systemctl restart tomcat9
The "Sign in with OpenID" button will appear on the Guacamole login page.
6.6 Create a permanent admin in Keycloak
Switch back to the master realm, create a user (e.g. massiadmin) and assign them the admin role via Role mapping. Once logged in with this account, delete the temporary admin account.
7. Automatic Keycloak → Guacamole synchronization
The following script retrieves users from the Keycloak realm and creates them in Guacamole if they don't already exist. The identifier used is the email (consistent with OpenID authentication).
#!/usr/bin/env python3
"""
sync_keycloak_guacamole.py
Synchronizes Keycloak users → Guacamole.
Suggested cron: 0 * * * * /usr/bin/python3 /opt/scripts/sync_keycloak_guacamole.py \
>> /var/log/sync_keycloak_guacamole.log 2>&1
"""
import requests
import logging
# ── Configuration ──────────────────────────────────────────────────────────────
KEYCLOAK_URL = "http://<IP>:8180"
KEYCLOAK_REALM = "guacamole"
KEYCLOAK_ADMIN_REALM = "master"
KEYCLOAK_ADMIN_USER = "massiadmin"
KEYCLOAK_ADMIN_PASS = "YOUR_PASSWORD" # ← change this
GUACAMOLE_URL = "http://<IP>:8080/guacamole"
GUACAMOLE_ADMIN = "guacadmin"
GUACAMOLE_PASS = "YOUR_PASSWORD" # ← change this
GUACAMOLE_DATASOURCE = "mysql"
# ── Logging ────────────────────────────────────────────────────────────────────
logging.basicConfig(level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S")
log = logging.getLogger(__name__)
# ── Keycloak ───────────────────────────────────────────────────────────────────
def get_keycloak_token():
url = f"{KEYCLOAK_URL}/realms/{KEYCLOAK_ADMIN_REALM}/protocol/openid-connect/token"
r = requests.post(url, data={
"client_id": "admin-cli",
"username": KEYCLOAK_ADMIN_USER,
"password": KEYCLOAK_ADMIN_PASS,
"grant_type": "password"
})
r.raise_for_status()
log.info("Keycloak token obtained.")
return r.json()["access_token"]
def get_keycloak_users(token):
url = f"{KEYCLOAK_URL}/admin/realms/{KEYCLOAK_REALM}/users?max=1000"
r = requests.get(url, headers={"Authorization": f"Bearer {token}"})
r.raise_for_status()
users = r.json()
log.info(f"{len(users)} user(s) found in Keycloak.")
return users
# ── Guacamole ──────────────────────────────────────────────────────────────────
def get_guacamole_token():
r = requests.post(f"{GUACAMOLE_URL}/api/tokens",
data={"username": GUACAMOLE_ADMIN, "password": GUACAMOLE_PASS},
headers={"Content-Type": "application/x-www-form-urlencoded"})
r.raise_for_status()
log.info("Guacamole token obtained.")
return r.json()["authToken"]
def get_guacamole_users(token):
r = requests.get(
f"{GUACAMOLE_URL}/api/session/data/{GUACAMOLE_DATASOURCE}/users",
headers={"Guacamole-Token": token})
r.raise_for_status()
usernames = list(r.json().keys())
log.info(f"{len(usernames)} user(s) in Guacamole.")
return usernames
def create_guacamole_user(token, email):
r = requests.post(
f"{GUACAMOLE_URL}/api/session/data/{GUACAMOLE_DATASOURCE}/users",
headers={"Guacamole-Token": token, "Content-Type": "application/json"},
json={
"username": email,
"password": "", # Auth via Keycloak, no local password
"attributes": {
"disabled": "", "expired": "",
"access-window-start": "", "access-window-end": "",
"valid-from": "", "valid-until": "", "timezone": ""
}
})
if r.status_code == 200:
log.info(f" Created in Guacamole: {email}")
return True
log.error(f" Error {r.status_code} for {email}: {r.text}")
return False
# ── Synchronization ────────────────────────────────────────────────────────────
def sync():
log.info("=" * 55)
log.info("Starting Keycloak → Guacamole synchronization")
log.info("=" * 55)
kc_token = get_keycloak_token()
guac_token = get_guacamole_token()
kc_users = get_keycloak_users(kc_token)
guac_users = get_guacamole_users(guac_token)
created = skipped = 0
for user in kc_users:
email = user.get("email", "")
if not email:
log.warning(f"User {user.get('username')} has no email, skipping.")
skipped += 1
continue
if email in guac_users:
log.info(f" Already exists: {email}")
skipped += 1
else:
created += 1 if create_guacamole_user(guac_token, email) else 0
log.info(f"Done: {created} created, {skipped} skipped.")
if __name__ == "__main__":
sync()
Automate via cron:
sudo crontab -e
# Add:
0 * * * * /usr/bin/python3 /opt/scripts/sync_keycloak_guacamole.py >> /var/log/sync_keycloak_guacamole.log 2>&1
Verify created users:
mysql -u guacamole_user -p guacamole_db \
-e "SELECT * FROM guacamole_entity WHERE type='USER';"
8. Secret management with HashiCorp Vault
Instead of storing SSH/RDP passwords in plain text in Guacamole, HashiCorp Vault centralizes them and enables automatic rotation.
Target architecture:
User → Keycloak (SSO) → Guacamole → Vault (SSH/RDP secrets) → Target server
8.1 Install Vault
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg \
--dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault
vault version
8.2 Start Vault (dev mode for testing)
# Terminal 1
vault server -dev -dev-listen-address="0.0.0.0:8200"
# Note the Unseal Key and Root Token displayed
# Terminal 2
export VAULT_ADDR='http://127.0.0.1:8200'
vault status
vault login # Paste the Root Token
8.3 Store SSH/RDP secrets
# Enable the KV v2 engine for Guacamole
vault secrets enable -path=guacamole kv-v2
# Create an SSH secret
vault kv put guacamole/ssh/my-server \
hostname="192.168.1.23" \
port="22" \
username="ubuntu" \
password='MyPassword!'
# Read a secret
vault kv get guacamole/ssh/my-server
Note:
devmode does not persist data across restarts. For production, configure Vault with a storage backend (Raft, Consul…) and enable auto-unseal.
9. Useful scripts
9.1 Create an SSH connection and assign it to a user
#!/usr/bin/env python3
"""
guac_ssh.py — Creates an SSH connection in Guacamole and assigns it to a user.
Usage: python3 guac_ssh.py
"""
import requests
import logging
GUACAMOLE_URL = "http://<IP>:8080/guacamole"
GUACAMOLE_ADMIN = "guacadmin"
GUACAMOLE_PASS = "YOUR_PASSWORD" # ← change this
GUACAMOLE_DATASOURCE = "mysql"
TARGET_USER = "test@test.com"
SSH_CONNECTION = {
"name": "My SSH Server",
"hostname": "192.168.1.23",
"port": "22",
"username": "ubuntu",
"password": "lala1234!"
}
logging.basicConfig(level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)
def get_token():
r = requests.post(f"{GUACAMOLE_URL}/api/tokens",
data={"username": GUACAMOLE_ADMIN, "password": GUACAMOLE_PASS},
headers={"Content-Type": "application/x-www-form-urlencoded"})
r.raise_for_status()
return r.json()["authToken"]
def create_connection(token):
r = requests.post(
f"{GUACAMOLE_URL}/api/session/data/{GUACAMOLE_DATASOURCE}/connections",
headers={"Guacamole-Token": token, "Content-Type": "application/json"},
json={
"parentIdentifier": "ROOT",
"name": SSH_CONNECTION["name"],
"protocol": "ssh",
"parameters": {
"hostname": SSH_CONNECTION["hostname"],
"port": SSH_CONNECTION["port"],
"username": SSH_CONNECTION["username"],
"password": SSH_CONNECTION["password"],
"recording-path": "${HISTORY_PATH}/${HISTORY_UUID}",
"create-recording-path": "true",
"recording-include-keys": "true",
"recording-exclude-mouse":"false",
"recording-exclude-output":"false"
},
"attributes": {"max-connections": "5", "max-connections-per-user": "1"}
})
if r.status_code == 200:
cid = r.json()["identifier"]
log.info(f" Connection created (ID: {cid})")
return cid
log.error(f" {r.status_code} — {r.text}")
return None
def assign_to_user(token, connection_id, username):
r = requests.patch(
f"{GUACAMOLE_URL}/api/session/data/{GUACAMOLE_DATASOURCE}/users/{username}/permissions",
headers={"Guacamole-Token": token, "Content-Type": "application/json"},
json=[{"op": "add", "path": f"/connectionPermissions/{connection_id}", "value": "READ"}])
if r.status_code == 204:
log.info(f" Connection {connection_id} assigned to {username}")
else:
log.error(f" {r.status_code} — {r.text}")
if __name__ == "__main__":
token = get_token()
if token:
cid = create_connection(token)
if cid:
assign_to_user(token, cid, TARGET_USER)
10. References
| Resource | Link |
|---|---|
| Guacamole — OpenID Connect | https://guacamole.apache.org/doc/gug/openid-auth.html |
| Guacamole — Session Recording | https://guacamole.apache.org/doc/gug/recording-playback.html |
| Keycloak — Official documentation | https://www.keycloak.org/documentation |
| Keycloak — OIDC Clients | https://www.keycloak.org/docs/latest/server_admin/#oidc-clients |
| HashiCorp Vault — Installation | https://developer.hashicorp.com/vault/install |
| Apache Guacamole — Support | https://guacamole.apache.org/support/ |
Written by Massioudath BANKOLE — The Journal of an Ethical Hacker