#!/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)"