Skip to content

Instantly share code, notes, and snippets.

@u1i
Last active June 1, 2025 04:50
Show Gist options
  • Save u1i/ea5749227103def040cd13838c608c7c to your computer and use it in GitHub Desktop.
Save u1i/ea5749227103def040cd13838c608c7c to your computer and use it in GitHub Desktop.
Guacamole RDP Web

Apache Guacamole Multi-User Setup Guide

Complete setup for 20 concurrent users with web-based remote desktop access via Apache Guacamole, RDP, and Cloudflare tunnel on Ubuntu 24.04 LTS.

Prerequisites

  • Bare Ubuntu 24.04 LTS server
  • SSH access as root
  • Domain configured with Cloudflare DNS
  • Minimum 8GB RAM, 4+ CPU cores recommended for 20 concurrent users

Architecture Overview

  • Frontend: Apache Guacamole web interface (browser-based)
  • Backend: xrdp providing RDP sessions with XFCE desktop
  • Database: PostgreSQL for user/connection management
  • Containers: Podman containers for Guacamole services
  • Tunnel: Cloudflare tunnel for secure HTTPS access
  • Users: 20 individual accounts with dedicated desktop sessions

Step 1: System Update and Base Packages

apt update
apt upgrade -y
apt install curl wget vim net-tools lsof

Step 2: Install Desktop Environment

Install XFCE desktop environment:

apt install xfce4 xfce4-goodies xorg dbus-x11 x11-xserver-utils

Step 3: Install and Configure RDP Server

Install xrdp:

apt install xrdp
systemctl enable xrdp
systemctl start xrdp

Configure xrdp to use XFCE:

echo "startxfce4" > /etc/skel/.xsession
chmod +x /etc/skel/.xsession

Verify xrdp is running:

systemctl status xrdp
netstat -tlnp | grep 3389

Step 4: Create RDP User Accounts

Create 20 system users for RDP access:

for i in {1..20}; do
    useradd -m -s /bin/bash user$i
    echo "user$i:UserPass$i" | chpasswd
    cp /etc/skel/.xsession /home/user$i/
    chown user$i:user$i /home/user$i/.xsession
    echo "Created user$i with password UserPass$i"
done

Step 5: Install Podman

apt install podman

Configure container registries:

cat >> /etc/containers/registries.conf << 'EOF'

[registries.search]
registries = ['docker.io', 'quay.io']
EOF

Step 6: Setup Guacamole with Podman

Create Guacamole directory:

mkdir -p /opt/guacamole
cd /opt/guacamole

Create podman network:

podman network create guacamole-net

Start PostgreSQL database:

podman run -d --name guacamole-postgres \
  --network guacamole-net \
  -e POSTGRES_DB=guacamole_db \
  -e POSTGRES_USER=guacamole_user \
  -e POSTGRES_PASSWORD=guacamole_pass \
  docker.io/postgres:13

Wait for database to initialize:

sleep 15

Initialize Guacamole database schema:

# Generate the database schema
podman run --rm docker.io/guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgresql > /tmp/schema.sql

# Copy schema to database container
podman cp /tmp/schema.sql guacamole-postgres:/tmp/schema.sql

# Apply schema to database
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -f /tmp/schema.sql

# Verify schema was applied (should show multiple tables)
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "\dt"

Start Guacamole daemon:

podman run -d --name guacd \
  --network guacamole-net \
  docker.io/guacamole/guacd:latest

Start Guacamole web application:

podman run -d --name guacamole \
  --network guacamole-net \
  -p 8080:8080 \
  -e GUACD_HOSTNAME=guacd \
  -e POSTGRESQL_DATABASE=guacamole_db \
  -e POSTGRESQL_USER=guacamole_user \
  -e POSTGRESQL_PASSWORD=guacamole_pass \
  -e POSTGRESQL_HOSTNAME=guacamole-postgres \
  docker.io/guacamole/guacamole:latest

Verify all containers are running:

podman ps

Test Guacamole web interface:

curl -I localhost:8080/guacamole/

Step 7: Configure Host Networking (Fix Container-to-Host Communication)

Stop current containers:

podman stop guacamole guacd guacamole-postgres
podman rm guacamole guacd guacamole-postgres

Restart with host networking:

# PostgreSQL
podman run -d --name guacamole-postgres \
  --network host \
  -e POSTGRES_DB=guacamole_db \
  -e POSTGRES_USER=guacamole_user \
  -e POSTGRES_PASSWORD=guacamole_pass \
  docker.io/postgres:13

# Wait for database
sleep 15

# Reinitialize database schema (CRITICAL STEP)
podman run --rm docker.io/guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgresql > /tmp/schema.sql
podman cp /tmp/schema.sql guacamole-postgres:/tmp/schema.sql
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -f /tmp/schema.sql

# Verify tables exist (should show guacamole_user, guacamole_connection, etc.)
podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "\dt"

# Guacamole daemon
podman run -d --name guacd \
  --network host \
  docker.io/guacamole/guacd:latest

# Guacamole web app
podman run -d --name guacamole \
  --network host \
  -e GUACD_HOSTNAME=localhost \
  -e POSTGRESQL_DATABASE=guacamole_db \
  -e POSTGRESQL_USER=guacamole_user \
  -e POSTGRESQL_PASSWORD=guacamole_pass \
  -e POSTGRESQL_HOSTNAME=localhost \
  docker.io/guacamole/guacamole:latest

Step 8: Test Single Connection

Access Guacamole web interface:

  1. Open browser to http://localhost:8080/guacamole/
  2. Login with guacadmin / guacadmin
  3. Go to Settings → Connections → New Connection
  4. Create test connection:
    • Name: User 1 Desktop
    • Protocol: RDP
    • Hostname: localhost
    • Port: 3389
    • Username: user1
    • Password: UserPass1
  5. Save and test the connection

Step 9: Bulk Create Users and Connections

Install Python dependencies:

apt install python3-pip
pip3 install requests

Create the bulk setup script:

cat > /opt/guacamole/bulk_setup.py << 'EOF'
#!/usr/bin/env python3
"""
Bulk setup script for Guacamole users and RDP connections
Creates 20 users with their own login and dedicated RDP connection
"""

import requests
import json
import sys

# Configuration
GUACAMOLE_URL = "http://localhost:8080/guacamole"
ADMIN_USER = "guacadmin"
ADMIN_PASS = "guacadmin"

def get_auth_token():
    """Get authentication token from Guacamole"""
    auth_url = f"{GUACAMOLE_URL}/api/tokens"
    auth_data = {
        "username": ADMIN_USER,
        "password": ADMIN_PASS
    }
    
    response = requests.post(auth_url, data=auth_data)
    if response.status_code != 200:
        print(f"Failed to authenticate: {response.status_code}")
        sys.exit(1)
    
    return response.json()["authToken"]

def create_user(token, username, password):
    """Create a new Guacamole user"""
    headers = {"Content-Type": "application/json"}
    user_url = f"{GUACAMOLE_URL}/api/session/data/mysql/users?token={token}"
    
    user_data = {
        "username": username,
        "password": password,
        "attributes": {
            "disabled": "",
            "expired": "",
            "access-window-start": "",
            "access-window-end": "",
            "valid-from": "",
            "valid-until": "",
            "timezone": "",
            "full-name": f"User {username[-2:]}",
            "email-address": "",
            "organization": "",
            "organizational-role": ""
        }
    }
    
    response = requests.post(user_url, headers=headers, data=json.dumps(user_data))
    if response.status_code == 200:
        print(f"✓ Created user: {username}")
        return True
    else:
        print(f"✗ Failed to create user {username}: {response.status_code}")
        return False

def create_connection(token, name, rdp_username, rdp_password):
    """Create an RDP connection"""
    headers = {"Content-Type": "application/json"}
    conn_url = f"{GUACAMOLE_URL}/api/session/data/mysql/connections?token={token}"
    
    connection_data = {
        "name": name,
        "protocol": "rdp",
        "parameters": {
            "hostname": "localhost",
            "port": "3389",
            "username": rdp_username,
            "password": rdp_password,
            "security": "any",
            "ignore-cert": "true",
            "color-depth": "32",
            "width": "1024",
            "height": "768",
            "dpi": "96"
        },
        "attributes": {
            "max-connections": "1",
            "max-connections-per-user": "1",
            "weight": "",
            "failover-only": "",
            "guacd-port": "",
            "guacd-encryption": "",
            "guacd-hostname": ""
        }
    }
    
    response = requests.post(conn_url, headers=headers, data=json.dumps(connection_data))
    if response.status_code == 200:
        print(f"✓ Created connection: {name}")
        return response.json()["identifier"]
    else:
        print(f"✗ Failed to create connection {name}: {response.status_code}")
        return None

def grant_connection_permission(token, username, connection_id):
    """Grant user permission to access their connection"""
    headers = {"Content-Type": "application/json"}
    perm_url = f"{GUACAMOLE_URL}/api/session/data/mysql/users/{username}/permissions?token={token}"
    
    permissions = [
        {
            "op": "add",
            "path": f"/connectionPermissions/{connection_id}",
            "value": "READ"
        }
    ]
    
    response = requests.patch(perm_url, headers=headers, data=json.dumps(permissions))
    if response.status_code == 204:
        print(f"✓ Granted connection permission to {username}")
        return True
    else:
        print(f"✗ Failed to grant permission to {username}: {response.status_code}")
        return False

def main():
    print("Starting Guacamole bulk setup...")
    print("=" * 50)
    
    # Get authentication token
    print("Authenticating with Guacamole...")
    token = get_auth_token()
    print("✓ Authentication successful")
    print()
    
    # Create 20 users and connections
    for i in range(1, 21):
        user_num = f"{i:02d}"  # 01, 02, 03, etc.
        guac_username = f"user{user_num}"
        guac_password = f"GuacPass{i}"
        rdp_username = f"user{i}"
        rdp_password = f"UserPass{i}"
        connection_name = f"Desktop {i}"
        
        print(f"Setting up User {i}...")
        
        # Create Guacamole user account
        if create_user(token, guac_username, guac_password):
            
            # Create RDP connection
            connection_id = create_connection(token, connection_name, rdp_username, rdp_password)
            
            if connection_id:
                # Grant user permission to their connection
                grant_connection_permission(token, guac_username, connection_id)
        
        print()
    
    print("=" * 50)
    print("Setup complete!")
    print()
    print("User Access Information:")
    print("=" * 50)
    print("URL: https://subdomain.yourdomain.com/guacamole/")
    print()
    print("User Credentials:")
    for i in range(1, 21):
        user_num = f"{i:02d}"
        print(f"User {user_num}: username=user{user_num}, password=GuacPass{i}")
    print()
    print("Each user will see their own 'Desktop X' connection after login.")

if __name__ == "__main__":
    # Check if requests is available
    try:
        import requests
    except ImportError:
        print("Error: 'requests' module not found.")
        print("Install it with: pip3 install requests")
        sys.exit(1)
    
    main()
EOF

chmod +x /opt/guacamole/bulk_setup.py

Run the bulk setup:

cd /opt/guacamole
python3 bulk_setup.py

Step 10: Install Cloudflared

wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
dpkg -i cloudflared-linux-amd64.deb

Step 11: Setup Cloudflare Tunnel

Authenticate with Cloudflare:

cloudflared tunnel login

Create tunnel:

cloudflared tunnel create guacamole-tunnel

Configure tunnel (replace YOUR_TUNNEL_ID with actual tunnel ID):

cat > ~/.cloudflared/config.yml << 'EOF'
tunnel: YOUR_TUNNEL_ID
credentials-file: /root/.cloudflared/YOUR_TUNNEL_ID.json

ingress:
  - hostname: subdomain.yourdomain.com
    service: http://localhost:8080
  - service: http_status:404
EOF

Add DNS record:

cloudflared tunnel route dns guacamole-tunnel subdomain.yourdomain.com

Start tunnel:

cloudflared tunnel run guacamole-tunnel

Step 12: Create Systemd Services (Production)

Create Guacamole service:

cat > /etc/systemd/system/guacamole.service << 'EOF'
[Unit]
Description=Guacamole Containers
After=network.target

[Service]
Type=forking
RemainAfterExit=yes
ExecStart=/bin/bash -c '\
podman run -d --name guacamole-postgres \
  --network host \
  -e POSTGRES_DB=guacamole_db \
  -e POSTGRES_USER=guacamole_user \
  -e POSTGRES_PASSWORD=guacamole_pass \
  docker.io/postgres:13 && \
sleep 10 && \
podman run -d --name guacd \
  --network host \
  docker.io/guacamole/guacd:latest && \
podman run -d --name guacamole \
  --network host \
  -e GUACD_HOSTNAME=localhost \
  -e POSTGRESQL_DATABASE=guacamole_db \
  -e POSTGRESQL_USER=guacamole_user \
  -e POSTGRESQL_PASSWORD=guacamole_pass \
  -e POSTGRESQL_HOSTNAME=localhost \
  docker.io/guacamole/guacamole:latest'
ExecStop=/bin/bash -c '\
podman stop guacamole guacd guacamole-postgres && \
podman rm guacamole guacd guacamole-postgres'
Restart=always

[Install]
WantedBy=multi-user.target
EOF

Create Cloudflare tunnel service:

cat > /etc/systemd/system/cloudflared.service << 'EOF'
[Unit]
Description=Cloudflare Tunnel
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/cloudflared tunnel run guacamole-tunnel
Restart=always

[Install]
WantedBy=multi-user.target
EOF

Enable and start services:

systemctl enable guacamole cloudflared xrdp
systemctl start guacamole cloudflared

User Access

For End Users:

  1. Navigate to: https://subdomain.yourdomain.com/guacamole/
  2. Login credentials:
    • User 01: user01 / GuacPass1
    • User 02: user02 / GuacPass2
    • ...
    • User 20: user20 / GuacPass20
  3. Click their desktop connection (e.g., "Desktop 1")
  4. Access their XFCE desktop directly in the browser

User Desktop Sessions:

  • Each user gets a dedicated XFCE desktop session
  • Sessions persist between browser connections
  • Full desktop environment with applications
  • Copy/paste between local machine and remote desktop
  • File transfer capabilities

Management Commands

Check all services:

systemctl status xrdp guacamole cloudflared
podman ps

View logs:

journalctl -u guacamole -f
journalctl -u cloudflared -f
podman logs guacamole

Restart services:

systemctl restart guacamole
systemctl restart cloudflared

Add more users:

# Add system user
useradd -m -s /bin/bash userX
echo "userX:UserPassX" | chpasswd
cp /etc/skel/.xsession /home/userX/
chown userX:userX /home/userX/.xsession

# Add to Guacamole via web interface or modify bulk_setup.py

Security Considerations

  • Network Security: All traffic encrypted via Cloudflare tunnel
  • User Isolation: Each user has separate system account and RDP session
  • Authentication: Multi-layer authentication (Guacamole + RDP)
  • Access Control: Users can only access their assigned desktop
  • Session Management: Automatic session cleanup on disconnect

Common Issues and Solutions:

"ERROR: relation 'guacamole_user' does not exist"

  • This means the database schema wasn't properly initialized
  • Stop Guacamole: podman stop guacamole
  • Reinitialize database:
    # Drop and recreate database
    podman exec -it guacamole-postgres psql -U guacamole_user -d postgres -c "DROP DATABASE IF EXISTS guacamole_db;"
    podman exec -it guacamole-postgres psql -U guacamole_user -d postgres -c "CREATE DATABASE guacamole_db;"
    
    # Apply fresh schema
    podman run --rm docker.io/guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgresql > /tmp/schema.sql
    podman cp /tmp/schema.sql guacamole-postgres:/tmp/schema.sql
    podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -f /tmp/schema.sql
    
    # Restart Guacamole
    podman start guacamole

"An error has occurred and this action cannot be completed"

  • Usually indicates database connection or schema issues
  • Check Guacamole logs: podman logs guacamole
  • Verify database schema exists: podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "\dt"
  • If no tables exist, reinitialize schema as above

Guacamole web interface not accessible:

  • Check containers: podman ps
  • Check logs: podman logs guacamole
  • Verify port 8080: netstat -tlnp | grep 8080

RDP connection fails:

  • Check xrdp status: systemctl status xrdp
  • Verify user credentials
  • Check system user exists: id userX

Cloudflare tunnel issues:

  • Verify tunnel config: cat ~/.cloudflared/config.yml
  • Check DNS record: nslookup subdomain.yourdomain.com
  • Review logs: journalctl -u cloudflared

Database connection errors:

  • Restart PostgreSQL container: podman restart guacamole-postgres
  • Check database schema exists:
    podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "\dt"
  • Reinitialize schema if tables missing:
    # Generate fresh schema
    podman run --rm docker.io/guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgresql > /tmp/fresh_schema.sql
    
    # Apply to database
    podman cp /tmp/fresh_schema.sql guacamole-postgres:/tmp/fresh_schema.sql
    podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -f /tmp/fresh_schema.sql
    
    # Restart Guacamole after schema fix
    podman restart guacamole
  • Check database connectivity: podman exec -it guacamole-postgres psql -U guacamole_user -d guacamole_db -c "SELECT version();"

Performance Optimization

For 20+ concurrent users:

  • RAM: 16GB+ recommended (1GB per concurrent user)
  • CPU: 8+ cores recommended
  • Storage: SSD recommended for user home directories
  • Network: 1Gbps+ connection for optimal performance

System tuning:

# Increase file descriptors
echo '* soft nofile 65536' >> /etc/security/limits.conf
echo '* hard nofile 65536' >> /etc/security/limits.conf

# Optimize kernel parameters
echo 'net.core.somaxconn = 65536' >> /etc/sysctl.conf
echo 'net.ipv4.tcp_max_syn_backlog = 65536' >> /etc/sysctl.conf
sysctl -p

Maintenance

Regular tasks:

  • Monitor disk usage in user home directories
  • Check container logs for errors
  • Update containers periodically
  • Backup Guacamole database
  • Monitor system resources

Backup database:

podman exec guacamole-postgres pg_dump -U guacamole_user guacamole_db > guacamole_backup.sql

Restore database:

podman exec -i guacamole-postgres psql -U guacamole_user -d guacamole_db < guacamole_backup.sql

Summary

This setup provides a complete, scalable solution for 20 concurrent users to access individual XFCE desktop sessions via web browser. The architecture uses modern containerization with Podman, secure tunneling via Cloudflare, and efficient RDP for desktop sessions.

Key Features:

  • Browser-based access (no client software required)
  • Individual user accounts and isolated sessions
  • Secure HTTPS access via Cloudflare tunnel
  • Persistent desktop sessions
  • Scalable architecture
  • Full XFCE desktop environment
  • Copy/paste and file transfer support
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment