Guacamole Host Setup
- !/usr/bin/env bash
set -euo pipefail
- --- Prompt Section ---
read -rp "Enter your domain (FQDN): " DOMAIN read -rp "Enter email for Let's Encrypt: " EMAIL read -rp "Enter the VNC username to create and configure: " VNC_USER
- Generate random passwords
ADMIN_PASS="$(openssl rand -base64 12)" DB_ROOT_PASS="$(openssl rand -base64 24 | tr -d '=+/')" DB_GUAC_PASS="$(openssl rand -base64 24 | tr -d '=+/')"
- --- Install dependencies (Debian/Ubuntu) ---
echo "[INFO] Installing required packages..." apt update apt install -y docker.io docker-compose-plugin nginx certbot python3-certbot-nginx ufw \
curl whois net-tools xfce4 xfce4-goodies tightvncserver unzip
- Decide compose command
if command -v docker-compose >/dev/null 2>&1; then
COMPOSE="docker-compose"
else
COMPOSE="docker compose"
fi
- --- Layout ---
APPDIR=/opt/guacamole NGXCONF=/etc/nginx/sites-enabled WEBROOT=/var/www/html ENVFILE="$APPDIR/.env" COMPOSEFILE="$APPDIR/docker-compose.yml" INITDIR="$APPDIR/initdb" GUAC_HOME="$APPDIR/guac-home"
mkdir -p "$APPDIR" "$INITDIR" "$GUAC_HOME/extensions" "$GUAC_HOME/lib"
- --- .env with secrets ---
install -m 0600 -o root -g root /dev/null "$ENVFILE" cat > "$ENVFILE" <<EOF GUAC_VERSION=1.5.5
- MariaDB
MYSQL_DATABASE=guacamole_db MYSQL_USER=guacamole_user MYSQL_PASSWORD=$DB_GUAC_PASS MYSQL_ROOT_PASSWORD=$DB_ROOT_PASS
- Paths
APPDIR=$APPDIR GUAC_HOME=$GUAC_HOME EOF chmod 600 "$ENVFILE"
- --- Docker network ---
docker network create guac-network >/dev/null 2>&1 || true
- --- docker-compose.yml ---
cat > "$COMPOSEFILE" <<'YAML' services:
mariadb: image: mariadb:10.11 restart: unless-stopped environment: MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} volumes: - ${APPDIR}/mariadb:/var/lib/mysql - ${APPDIR}/initdb:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u${MYSQL_USER} -p${MYSQL_PASSWORD} || exit 1"] interval: 10s timeout: 5s retries: 20 networks: [guac-network] guacd: image: guacamole/guacd:${GUAC_VERSION} restart: unless-stopped healthcheck: test: ["CMD-SHELL", "nc -z 127.0.0.1 4822 || exit 1"] interval: 10s timeout: 5s retries: 20 networks: [guac-network] guacamole: image: guacamole/guacamole:${GUAC_VERSION} restart: unless-stopped depends_on: mariadb: condition: service_healthy guacd: condition: service_healthy environment: GUACD_HOSTNAME: guacd MYSQL_HOSTNAME: mariadb MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} GUACAMOLE_HOME: /guac-home volumes: - ${GUAC_HOME}:/guac-home healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/guacamole/ || exit 1"] interval: 15s timeout: 5s retries: 20 networks: [guac-network]
networks:
guac-network: external: true
YAML
- --- Initialize DB schema & JDBC extension ---
echo "[INFO] Fetching JDBC auth package..." cd "$APPDIR" curl -fsSLo guacamole-auth-jdbc.tgz \
https://downloads.apache.org/guacamole/${GUAC_VERSION}/binary/guacamole-auth-jdbc-${GUAC_VERSION}.tar.gz
tar -xzf guacamole-auth-jdbc.tgz cp -f guacamole-auth-jdbc-${GUAC_VERSION}/mysql/*.sql "$INITDIR/" cp -f guacamole-auth-jdbc-${GUAC_VERSION}/mysql/guacamole-auth-jdbc-mysql-${GUAC_VERSION}.jar "$GUAC_HOME/extensions/"
- --- Bring up DB & guacd so initdb runs ---
$COMPOSE --env-file "$ENVFILE" -f "$COMPOSEFILE" up -d mariadb guacd echo "[INFO] Waiting for MariaDB to be healthy..." $COMPOSE --env-file "$ENVFILE" -f "$COMPOSEFILE" ps
- Start guacamole after DB is healthy
$COMPOSE --env-file "$ENVFILE" -f "$COMPOSEFILE" up -d guacamole
- --- Set guacadmin password in DB (salt + SHA-256) ---
echo "[INFO] Setting guacadmin password in DB..." SALT="$(openssl rand -hex 16)" HASH="$(printf '%s%s' "$ADMIN_PASS" "$SALT" | openssl dgst -sha256 -binary | od -An -t x1 | tr -d ' \n')"
docker exec -i "$($COMPOSE --env-file "$ENVFILE" -f "$COMPOSEFILE" ps -q mariadb)" \
mysql -uroot -p"$DB_ROOT_PASS" guacamole_db <<SQL
UPDATE guacamole_user
SET password_salt = UNHEX('$SALT'), password_hash = UNHEX('$HASH'), disabled = 0 WHERE username='guacadmin';
SQL
- --- Nginx ACME (HTTP) site for webroot ---
echo "[INFO] Preparing Nginx HTTP challenge site..." mkdir -p "$WEBROOT/.well-known/acme-challenge"
cat > "$NGXCONF/guac-acme.conf" <<EOF server {
listen 80; server_name $DOMAIN; location /.well-known/acme-challenge/ { root $WEBROOT; try_files \$uri =404; } location / { return 301 https://\$host\$request_uri; }
} EOF
nginx -t && systemctl reload nginx
- --- Obtain cert ---
echo "[INFO] Requesting Let's Encrypt certificate..." certbot certonly --webroot -w "$WEBROOT" -d "$DOMAIN" \
--agree-tos -m "$EMAIL" --no-eff-email --non-interactive
- --- Nginx TLS reverse proxy (WebSockets, HSTS) ---
echo "[INFO] Writing Nginx TLS proxy..." cat > "$NGXCONF/guacamole.conf" <<'EOF' map $http_upgrade $connection_upgrade { default upgrade; close; }
server {
listen 443 ssl http2; server_name __DOMAIN__; ssl_certificate /etc/letsencrypt/live/__DOMAIN__/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/__DOMAIN__/privkey.pem; # Security ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; # Keep ACME available on 443 as well (optional) location /.well-known/acme-challenge/ { root /var/www/html; } # Guacamole (Tomcat inside container exposes /guacamole/) location / { proxy_pass http://127.0.0.1:8080/guacamole/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; proxy_read_timeout 3600s; proxy_send_timeout 3600s; # WebSockets proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; }
}
- HTTP kept for redirect + ACME (certbot renews against 80)
server {
listen 80; server_name __DOMAIN__; location /.well-known/acme-challenge/ { root /var/www/html; } location / { return 301 https://$host$request_uri; }
} EOF
- Inject the actual domain into the file
sed -i "s/__DOMAIN__/$DOMAIN/g" "$NGXCONF/guacamole.conf"
nginx -t && systemctl reload nginx
- --- Firewall ---
ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp ufw --force enable
- --- Certbot renew (daily) ---
( crontab -l 2>/dev/null; echo "12 3 * * * certbot renew --quiet && systemctl reload nginx" ) | crontab -
- --- VNC user (optional workstation target) ---
echo "[INFO] Configuring VNC user: $VNC_USER" adduser --disabled-password --gecos "" "$VNC_USER" || true mkdir -p "/home/$VNC_USER/.vnc" echo "Set VNC password for user $VNC_USER:" su - "$VNC_USER" -c "vncpasswd"
cat > "/home/$VNC_USER/.vnc/xstartup" <<'EOS'
- !/bin/bash
xrdb $HOME/.Xresources startxfce4 & EOS chmod +x "/home/$VNC_USER/.vnc/xstartup" chown -R "$VNC_USER:$VNC_USER" "/home/$VNC_USER/.vnc"
cat > "/etc/systemd/system/vncserver@.service" <<'EOS' [Unit] Description=Start TightVNC server at startup After=network.target
[Service] Type=forking User=%i PAMName=login PIDFile=/home/%i/.vnc/%H:1.pid ExecStartPre=-/usr/bin/vncserver -kill :1 > /dev/null 2>&1 ExecStart=/usr/bin/vncserver :1 ExecStop=/usr/bin/vncserver -kill :1
[Install] WantedBy=multi-user.target EOS
systemctl daemon-reload systemctl enable vncserver@"$VNC_USER" systemctl start vncserver@"$VNC_USER"
- --- Done ---
echo echo "[INFO] Guacamole is ready at: https://$DOMAIN/" echo "[INFO] Login: guacadmin" echo "[INFO] Password: $ADMIN_PASS" echo "[INFO] GUACAMOLE_HOME: $GUAC_HOME" echo "[INFO] .env stored at: $ENVFILE (chmod 600)"