CVE Watch
Cybersécuritédevsecops11 min de lecture

Complete Apache Guacamole 1.6.0 Installation on Ubuntu Server

MB
Massioudath Bankole
8 juin 2026 · 447 vues

Stack covered: Guacamole 1.6.0 · MariaDB · Apache Tomcat 9 · Keycloak 26 (SSO OpenID Connect) · HashiCorp Vault · Python (automation scripts)


Table of Contents

  1. Compiling and installing guacamole-server
  2. Installing the web application (Tomcat + .war)
  3. Configuring the MariaDB database
  4. Configuring Guacamole
  5. Session recording
  6. SSO with Keycloak (OpenID Connect)
  7. Automatic Keycloak → Guacamole synchronization
  8. Secret management with HashiCorp Vault
  9. Useful scripts
  10. 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 of 0.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 = 4822

Then 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:

QuestionAnswer
Switch to unix_socket authenticationN
Change the root passwordY (strong password)
Remove anonymous usersY
Disallow root login remotelyY
Remove test databaseY
Reload privilege tablesY

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 guacadmin account.


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:

ParameterValue
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):

  1. Create a realm named guacamole

  2. In that realm, create a client:

    • Client type: OpenID Connect
    • Client ID: guacamole
    • Client authentication: Off (public client)
    • Authentication flow: Standard flow + Implicit flow
  3. In Login settings, fill in (replace <IP> with the server IP):

FieldValue
Root URLhttp://<IP>:8080/guacamole
Home URLhttp://<IP>:8080/guacamole
Valid redirect URIshttp://<IP>:8080/guacamole/*
Valid post logout redirect URIshttp://<IP>:8080/guacamole
Web originshttp://<IP>:8080

6.4 Create a test user

In the guacamole realm → UsersCreate new user:

  • Username: testuser
  • Email: test@test.com
  • First/Last name: Test User

Credentials tab → Set passwordTest1234! (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: dev mode 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

ResourceLink
Guacamole — OpenID Connecthttps://guacamole.apache.org/doc/gug/openid-auth.html
Guacamole — Session Recordinghttps://guacamole.apache.org/doc/gug/recording-playback.html
Keycloak — Official documentationhttps://www.keycloak.org/documentation
Keycloak — OIDC Clientshttps://www.keycloak.org/docs/latest/server_admin/#oidc-clients
HashiCorp Vault — Installationhttps://developer.hashicorp.com/vault/install
Apache Guacamole — Supporthttps://guacamole.apache.org/support/

Written by Massioudath BANKOLE — The Journal of an Ethical Hacker

devsecopslinux
Partager cet article

Commentaires (0)

Sois le premier à commenter !

Articles similaires