#!/bin/bash # ============================================================================== # Script: create_pma_gateway.sh # Purpose: Create a time-limited gateway URL for phpMyAdmin on Virtuozzo LLSMP. # Usage: create_pma_gateway.sh --validity=30 [--slug=myalias] # Outputs: Prints the generated URL. # # SECURITY FEATURES: # - Input validation prevents path traversal and injection attacks # - Secret key handling avoids shell expansion vulnerabilities # - HMAC-signed tokens with time-based expiration # - Automatic SSL certificate detection and configuration # - Comprehensive rewrite rules for phpMyAdmin protection # - Secure cookie-based access control # - Self-destructing gateway files # ============================================================================== set -euo pipefail # ============================================================================== # Prerequisites Check: Verify required sudo privileges # ============================================================================== if ! sudo -n true 2>/dev/null; then echo "ERROR: This script requires sudo privileges for:" >&2 echo " - File operations (chown, chmod, mkdir, ln)" >&2 echo " - Package installation (dnf install xmlstarlet)" >&2 echo " - Configuration management (xmlstarlet)" >&2 echo " - Service control (systemctl restart lsws)" >&2 echo "Please run with appropriate sudo access or configure passwordless sudo." >&2 exit 1 fi SLUG="" VALIDITY=30 # minutes for arg in "$@"; do case $arg in --slug=*) SLUG="${arg#*=}" ;; --validity=*) VALIDITY="${arg#*=}" ;; *) echo "ERROR: Unknown argument $arg"; exit 1 ;; esac done if [[ -z "$SLUG" ]]; then SLUG=$(openssl rand -hex 4) # 8-char random fi # ============================================================================== # Input Validation: Validate command-line arguments for security and robustness # ============================================================================== # Validate VALIDITY: must be a positive integer if ! [[ "$VALIDITY" =~ ^[0-9]+$ ]] || (( VALIDITY <= 0 )); then echo "ERROR: --validity must be a positive integer (e.g., --validity=30)" >&2 exit 1 fi # Validate SLUG: allow only alphanumeric characters, hyphens, and underscores # This prevents path traversal sequences (../) and special character injection if ! [[ "$SLUG" =~ ^[a-zA-Z0-9_-]{1,64}$ ]]; then echo "ERROR: --slug must contain only alphanumeric characters, hyphens, or underscores (max 64 chars)" >&2 exit 1 fi # Determine public hostname for gateway URL (main domain with valid SSL cert) # Try JELASTIC_ENV_DOMAIN first, then fallback to hostname detection if [[ -n "${JELASTIC_ENV_DOMAIN:-}" ]]; then PUBLIC_HOST="$JELASTIC_ENV_DOMAIN" else # Fallback: Extract domain from hostname (remove node prefix and subdomain) ENV_HOST=$(hostname -f) ENV_HOST=${ENV_HOST#node*-} # strip nodeXXXX- # Extract the main domain (e.g., newtestenv.mightybox.cloud from node123-newtestenv.mightybox.cloud) PUBLIC_HOST=$(echo "$ENV_HOST" | sed -E 's/^[^.]*\.([^.]+\.[^.]+)$/\1/') # If extraction failed, use the hostname as-is if [[ -z "$PUBLIC_HOST" ]] || [[ "$PUBLIC_HOST" == "$ENV_HOST" ]]; then PUBLIC_HOST="$ENV_HOST" fi echo "WARNING: JELASTIC_ENV_DOMAIN not set. Using detected hostname: $PUBLIC_HOST" >&2 fi # Use standard Jelastic LLSMP main document root MAIN_DOCROOT="/var/www/webroot/ROOT" # ============================================================================== # Step 1: Ensure xmlstarlet is installed for safe XML parsing # ============================================================================== if ! command -v xmlstarlet &> /dev/null; then echo "xmlstarlet not found. Installing for safe XML parsing..." >&2 if ! sudo dnf install -y xmlstarlet; then echo "FATAL: Failed to install xmlstarlet. Cannot safely read LiteSpeed config." >&2 exit 1 fi fi # ============================================================================== # Step 2: Dynamically read SSL configuration from main LiteSpeed config # ============================================================================== LITESPEED_CONFIG="/var/www/conf/httpd_config.xml" KEY_FILE_PATH="" CERT_FILE_PATH="" if [[ -f "$LITESPEED_CONFIG" ]]; then echo "Reading SSL configuration from LiteSpeed main config..." >&2 # Query the main HTTPS listener (port 443) for keyFile and certFile # This is the most specific and robust XPath # Correctly capture the output of xmlstarlet without modification KEY_FILE_PATH=$(sudo xmlstarlet sel -t -v \ "//httpServerConfig/listenerList/listener[name='HTTPS' and secure='1' and address='*:443'][1]/keyFile" \ "$LITESPEED_CONFIG" 2>/dev/null) CERT_FILE_PATH=$(sudo xmlstarlet sel -t -v \ "//httpServerConfig/listenerList/listener[name='HTTPS' and secure='1' and address='*:443'][1]/certFile" \ "$LITESPEED_CONFIG" 2>/dev/null) fi # ============================================================================== # Step 3: Implement fallback to default self-signed certificate # ============================================================================== if [[ -z "$KEY_FILE_PATH" ]] || [[ -z "$CERT_FILE_PATH" ]]; then echo "No custom SSL certificate found. Falling back to default self-signed certificate." >&2 # Use SINGLE quotes to write the literal string "$SERVER_ROOT" to the config, # not the shell variable. This is critical. KEY_FILE_PATH='$SERVER_ROOT/ssl/litespeed.key' CERT_FILE_PATH='$SERVER_ROOT/ssl/litespeed.crt' else echo "Using SSL certificate: $CERT_FILE_PATH" >&2 fi PMADB_DIR="/usr/share/phpMyAdmin" GATEWAY_FILE="$PMADB_DIR/access-db-$SLUG.php" PUBLIC_GATEWAY_FILE="$MAIN_DOCROOT/access-db-$SLUG.php" SECRET_FILE="/var/lib/jelastic/keys/mbadmin_secret" sudo mkdir -p "$(dirname $SECRET_FILE)" if [[ ! -f "$SECRET_FILE" ]]; then sudo sh -c "openssl rand -hex 32 > $SECRET_FILE" fi sudo chown litespeed:litespeed "$SECRET_FILE" sudo chmod 644 "$SECRET_FILE" # Validate the secret file to ensure it contains only valid hexadecimal characters if ! [[ "$(cat "$SECRET_FILE")" =~ ^[0-9a-f]{64}$ ]]; then echo "ERROR: Secret file $SECRET_FILE does not contain a valid 64-character hexadecimal string." >&2 exit 1 fi now=$(date +%s) expires=$((now + VALIDITY*60)) # token = base64("$SLUG:$expires") . '.' . HMAC_SHA256(secret, data) data="$SLUG:$expires" base=$(printf "%s" "$data" | base64 | tr -d '\n') # Security: Use a temporary file to avoid exposing $SECRET in shell expansion # This prevents the secret from being visible in process listings or shell history # Note: mktemp creates files with restrictive permissions (600) by default TEMP_SECRET_FILE=$(mktemp) trap "rm -f '$TEMP_SECRET_FILE'" EXIT sudo cat "$SECRET_FILE" > "$TEMP_SECRET_FILE" chmod 600 "$TEMP_SECRET_FILE" # Ensure only owner can read/write # Generate HMAC using PHP, reading secret directly from file to avoid injection risk mac=$(php -r " \$data = '$data'; \$secret = trim(file_get_contents('$SECRET_FILE')); if (!is_string(\$secret) || empty(\$secret)) { fwrite(STDERR, 'ERROR: Failed to read secret file\n'); exit(1); } \$hmac = hash_hmac('sha256', \$data, \$secret); if (!\$hmac) { fwrite(STDERR, 'ERROR: hash_hmac() failed\n'); exit(1); } echo \$hmac; " 2>&1) # Check PHP exit code and output php_exit_code=$? if [[ $php_exit_code -ne 0 ]] || [[ -z "$mac" ]] || [[ ${#mac} -ne 64 ]]; then echo "ERROR: Failed to generate HMAC token" >&2 echo "PHP error output: $mac" >&2 exit 1 fi token="$base.$mac" # Create symlinks for phpMyAdmin and gateway in main document root # This serves both through the main domain (port 443) with valid SSL certificate sudo mkdir -p "$MAIN_DOCROOT" # Symlink entire phpMyAdmin directory for full access sudo ln -sf "/usr/share/phpMyAdmin" "$MAIN_DOCROOT/phpmyadmin" # Symlink the gateway script for public access sudo ln -sf "$GATEWAY_FILE" "$PUBLIC_GATEWAY_FILE" # Remove the phpMyAdmin vhost to avoid exposing port 8443 publicly VHOST_CONFIG="/usr/share/phpMyAdmin/vhost.conf" if [[ -f "$VHOST_CONFIG" ]]; then echo "Removing phpMyAdmin vhost configuration to prevent public exposure on port 8443..." >&2 sudo rm -f "$VHOST_CONFIG" NEEDS_RESTART=1 fi # ============================================================================== # Step 4: Automatically inject security rules into main vhost configuration # ============================================================================== MAIN_VHOST_CONFIG="/var/www/conf/vhosts/$PUBLIC_HOST/vhconf.xml" # Ensure main vhost config exists (critical for automation) if [[ ! -f "$MAIN_VHOST_CONFIG" ]]; then echo "FATAL: Main vhost config not found at $MAIN_VHOST_CONFIG" >&2 echo "Expected location: /var/www/conf/vhosts/$PUBLIC_HOST/vhconf.xml" >&2 exit 1 fi MARKER="# PMA Gateway Security Rules" if ! sudo grep -qF "$MARKER" "$MAIN_VHOST_CONFIG"; then echo "Injecting PMA gateway security rules into main vhost..." >&2 # Define comprehensive security rules for phpMyAdmin protection NEW_RULES=$(cat <<'EOF' # PMA Gateway Security Rules # Allow access to the gateway scripts themselves RewriteCond %{REQUEST_URI} ^/access-db-.*\.php$ RewriteRule .* - [L] # Block all phpMyAdmin paths unless security cookie is present RewriteCond %{HTTP_COOKIE} !pma_access_granted RewriteCond %{REQUEST_URI} ^/(phpmyadmin/|index\.php|url\.php|js/|css/|libraries/|themes/|favicon\.ico) RewriteRule .* - [F,L] EOF ) # Enable rewrite and inject comprehensive rules if ! sudo xmlstarlet ed -L \ -u "//virtualHostConfig/rewrite/enable" -v "1" \ -u "//virtualHostConfig/rewrite/rules" -v "$(sudo xmlstarlet sel -t -v "//virtualHostConfig/rewrite/rules" "$MAIN_VHOST_CONFIG" 2>/dev/null)$NEW_RULES" \ "$MAIN_VHOST_CONFIG"; then echo "FATAL: Failed to update main vhost rewrite rules." >&2 exit 1 fi echo "✅ PMA security rules injected into main vhost configuration" >&2 NEEDS_RESTART=1 fi sudo tee "$GATEWAY_FILE" >/dev/null <<'PHP' intval($exp)) { unlink(__FILE__); // Self-destruct if expired deny(); } $secret = trim(file_get_contents('/var/lib/jelastic/keys/mbadmin_secret')); if (!hash_equals($sig, hash_hmac('sha256', $data, $secret))) { deny(); } // Issue a short-lived cookie that the rewrite rule looks for. // This cookie acts as the temporary pass. setcookie('pma_access_granted', $sig, intval($exp), '/', '', true, true); // Redirect to phpMyAdmin via the symlinked path header('Location: /phpmyadmin/index.php'); exit; ?> PHP sudo chown litespeed:litespeed "$GATEWAY_FILE" sudo chmod 644 "$GATEWAY_FILE" # Set proper permissions on the public symlink as well if [[ -L "$PUBLIC_GATEWAY_FILE" ]]; then sudo chown --no-dereference litespeed:litespeed "$PUBLIC_GATEWAY_FILE" sudo chmod 644 "$PUBLIC_GATEWAY_FILE" else echo "WARNING: Public symlink $PUBLIC_GATEWAY_FILE does not exist. Skipping chown/chmod." >&2 fi # Restart LiteSpeed if we modified the config if [[ "${NEEDS_RESTART:-0}" -eq 1 ]]; then echo "Applying security rules and restarting LiteSpeed..." >&2 if ! sudo systemctl restart lsws; then echo "Warning: LiteSpeed restart failed. Manual restart may be required." >&2 fi fi # Generate URL using public hostname (port 443) with valid SSL certificate # This bypasses CDN protections and uses the trusted certificate URL="https://$PUBLIC_HOST/access-db-$SLUG.php?token=$token" # Output JSON response for Cloud Scripting compatibility # Cloud Scripting expects structured JSON output from custom actions cat <&2 echo " • Gateway URL uses valid Let's Encrypt certificate" >&2 echo " • Served through main domain (port 443) with CDN protection" >&2 echo " • Port 8443 exposure has been removed for security" >&2 echo " • phpMyAdmin symlinked to /phpmyadmin/ with full protection" >&2 echo " • Security rules automatically injected into main vhost" >&2 echo "" >&2